关于强制转换:使用C ++,是否存在一种安全合法的方法来为涉及多重继承和虚拟继承的类层次结构实现“安全强制转换”而无需RTTI?

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从基类进行向下转换并不安全(至少在一般情况下如此),因为它们对对象数据/虚拟表布局是"盲目的"。是否可以通过遵循某些规则/限制来使它们"安全"?


协变返回类型允许子类返回与基本类的返回类型不同但继承的类型。如果从派生类进行调用,这将使您获得不同的返回值,但是在通过基类进行调用时,将获得正确的类型。

因此,在您的示例中,Derived返回的值的类型为Derived,但是当您通过Base指针调用该函数时,编译器必须在编译时决定是否可以合法地分配返回值。在运行时,将调用正确的foo()实例,因为foo()被声明为虚拟的,但是无论在运行时实际使用了什么Base的子类,编译器都必须使用相同的返回类型。这就是为什么协变返回类型不能完全任意的原因。也就是说,您不能在基类中返回int,而在派生类中不能返回string。派生类的返回类型必须是基类中返回的类型的子类型。

在这里可能值得注意的是,虽然您可以强制转换返回类型并正确编译所有内容,但您也可以强制转换指针并获得正确的值:

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可用,我也宁愿使用访客模式而不是强制转换和检查类型,或使用虚拟方法或其他方法实现行为。

进一步说明:整个解决方法的要点是基于以下假设:您按住Base*并想将其强制转换为Derived*,但是您不确定所持有的对象是否实际上是Derived*并且您想要某种检查,就像dynamic_cast那样。如果您知道派生类型是什么并且只想强制转换它,那么static_cast是可行的方法。 static_cast不会检查以确保您保存在基本指针中的运行时类型实际上是您要转换为的类型,但是它将进行编译时检查以确保转换是合法的。因此,例如您不能使用static_castint*转换为string*reinterpret_cast将允许这样做,但是您只是在强迫可能是一件坏事,而您不想这样做。