Using C++, is there a safe and legal way to implement a “safe cast” for a class hierarchy involving multiple and virtual inheritance without RTTI?
我目前正在使用元类型的系统来为我的项目的一小部分实施"安全转换",因为a)RTTI和dynamic_cast在我的项目环境中被有意禁用,并且b)这种功能将极大地简化和阐明码。
在我的项目中,有一个多层次的类层次结构,在某些情况下具有多重继承。但是,只有一个"根"类实际上被所有其他类直接或间接继承。 (这是层次结构中唯一的虚拟继承。)是否存在一种安全,合法的方式在不使用RTTI / dynamic_cast的情况下在这种层次结构中执行下转换?我已经在网上搜索并考虑了几种方法,但似乎都没有达到目标。就我而言,类层次结构是已知的-每个对象基本上都通过使用标签/枚举来知道其(元)类型及其祖先。 (是否还必须知道其他信息,例如基类初始化顺序,才能实现"安全强制转换"?)
我尝试过的一种方法是通过返回" this"指针的虚拟函数调用进行向下转换。从我的阅读(和我自己的经验)中,我了解到C ++中的" this"指针的类型与定义成员函数的类相对应,并将成员函数的cv限定条件应用于该指针。 。 (如此处所述=>此指针的类型)。我试图通过返回" this"指针来获得具有动态(运行时)对象类型的指针。由于我希望所有对象都实现此功能,因此在基类中使其成为纯虚拟函数,然后使用协变返回类型(实现类的指针)在派生类中对其进行定义。通过基本指针进行分派可以按预期方式工作,但是返回的指针似乎具有基于调用指针的类型,而不是所调用实现的类型。
这是说明问题的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <iostream> struct Base { virtual ~Base(){} virtual Base* foo( void ) = 0 ; } ; struct Derived : public Base { // <= XXX Derived* foo( void ){ std::cout <<"In Derived::foo()" << std::endl ; return this ; } void foo2( void ) { std::cout <<"In Derived::foo2()" << std::endl ; } } ; int main ( int argc, char* argv[] ) { Base* base = new Derived ; base->foo( ) ; // Dispatches to Derived::foo() // Derived* derived = base->foo( ) ; // PROBLEM! Derived* derived = static_cast< Derived* >( base->foo( ) ) ; // Works, as long as inheritance at XXX is non-virtual derived->foo2( ) ; delete base ; return 0 ; } |
如所写,上面的代码编译并运行没有问题。但是,如果我取消标记为"问题"的行的注释并注释掉其下的行,则会收到以下编译器错误(在cygwin环境中使用g ++版本4.83时-不是我的最终目标环境):
1 2 3 4 | test.cpp: In function ‘int main(int, char**)’: test.cpp:13:34: error: invalid conversion from ‘Base*’ to ‘Derived*’ [-fpermissive] Derived* derived = base->foo( ) ; // PROBLEM! ^ |
我已经在http://gcc.godbolt.org/的icc,clang和g ++版本中进行了尝试,并收到了类似的结果,因此我认为这不是编译器错误,我缺少了一些非常基本的东西。
具有协变返回类型的虚拟成员函数定义中返回的" this"指针的类型是什么?当我通过Base *调用foo()时,我显然执行了Derived :: foo()的实现,并且该实现成员函数声明为返回Derived *。那么,为什么要根据返回的指针执行赋值,则必须进行向下转换?使用static_cast是一个问题,因为在我的项目代码中确实有一个虚拟继承的基类,而static_cast将落在这个类上。我的印象是,使用C样式强制转换或reinterpret_cast从基类进行向下转换并不安全(至少在一般情况下如此),因为它们对对象数据/虚拟表布局是"盲目的"。是否可以通过遵循某些规则/限制来使它们"安全"?
协变返回类型允许子类返回与基本类的返回类型不同但继承的类型。如果从派生类进行调用,这将使您获得不同的返回值,但是在通过基类进行调用时,将获得正确的类型。
因此,在您的示例中,
在这里可能值得注意的是,虽然您可以强制转换返回类型并正确编译所有内容,但您也可以强制转换指针并获得正确的值:
1 | Derived* derived = static_cast< Derived* >(base)->foo( ); // OK |
现在回答您的原始问题,如果您的目标是从未知类型的基本指针开始,然后向下转换到派生指针,则将需要一些运行时信息。 RTTI就是这样做的,它使用与vtable一起存储的信息来确定实际类型。如果您没有RTTI,但是有一些嵌入式枚举,则可以使用它。我看不到您的课程,但类似的方法可以工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class Base { int myId; protected: Base(int id) : myId(id) public: template <class T> T* downcast() { if (myId == T::ID) { return static_cast<T*>(this); } return nullptr; } }; class Derived1 { public: static const int ID = 1; Derived1() : Base(ID) {} }; class Derived2 { public: static const int ID = 2; Derived2() : Base(ID) {} }; Base* b = new Derived1(); Derived1* d1 = b->downcast<Derived1>(); // Ok, returns a value Derived2* d2 = b->downcast<Derived2>(); // returns nullptr |
注意:这是为了回答我以为您在问的问题,但是我实际上并不认为这是一个好主意。正如评论中所建议的那样,即使我确实有RTTI可用,我也宁愿使用访客模式而不是强制转换和检查类型,或使用虚拟方法或其他方法实现行为。
进一步说明:整个解决方法的要点是基于以下假设:您按住