我目前正致力于实施"安全演员"对于我的项目的一小部分,使用元类型系统,因为a)在我的项目环境中故意禁用RTTI和dynamic_cast,b)这样的功能将极大地简化和澄清代码。
在我的项目中,有一个多级类层次结构,在某些情况下具有多重继承。但是,有一个“根”类几乎是由所有其他类直接或间接继承的。 (这是层次结构中唯一的虚拟继承。)是否有一种安全,合法的方法在不使用RTTI / dynamic_cast的情况下在这种层次结构中执行向下转换?我在网上搜索并考虑了几种方法,但似乎都没有达到标准。在我的例子中,类层次结构是已知的 - 每个对象都知道它们的(元)类型及其祖先,主要是通过使用标记/枚举。 (是否有其他信息,例如基类初始化顺序,必须知道实现“安全演员”?)
我尝试过的一种方法是通过一个返回“this”指针的虚函数调用进行向下转换。根据我所读过的内容(以及我自己的经验),我理解"这个" C ++中的指针是与定义成员函数的类相对应的类型,应用了成员函数的cv资格。 (如此处所述=> Type of 'this' pointer)。我试图通过返回" this"来获得指针具有动态(运行时)类型的对象。指针。因为我希望我的所有对象都实现这个函数,所以我在它的基类中使它成为一个纯虚函数,然后我使用一个协变返回类型(实现类的指针)在我的派生类中定义它。通过基指针调度按预期工作,但返回的指针似乎具有基于调用指针的类型,而不是调用的实现的类型。
以下是解释问题的代码:
#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 ;
}
如上所述,上面的代码编译并运行没有问题。但如果我取消注释标有&#34;问题&#34;并注释掉它下面的一行,我得到以下编译器错误(在cygwin环境中使用g ++版本4.83时 - 不是我的最终目标环境):
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 ++的版本并得到了类似的结果,所以我认为这不是编译器错误而且我错过了一些非常基础的东西。
返回的&#34;这个&#34;是什么类型?具有协变返回类型的虚拟成员函数定义的指针?当我通过Base *调用foo()时,我显然执行Derived :: foo()的实现,并且声明实现成员函数返回Derived *。那么为什么需要从返回的指针执行赋值所需的向下转换呢?使用static_cast是一个问题,因为我的项目代码中确实有一个虚拟继承的基类,而static_cast将会落在它上面。我的印象是,使用C风格的转换或reinterpret_cast进行基类的向下转换是不安全的(至少在一般情况下),因为它们对对象数据/虚拟表布局“盲目”。遵循一些规则/限制可以使这些变得“安全”吗?
答案 0 :(得分:2)
协变返回类型允许子类返回与基类的返回类型不同但继承的类型。这允许您在从派生类调用时返回不同的值,但在通过基类调用时可以获得正确的类型。
因此在您的示例中,Derived
返回的值的类型为Derived
,但是当您通过Base
指针调用该函数时,编译器必须决定在编译时是否可以合法分配返回的值。在运行时,将调用foo()
的正确实例,因为foo()
被声明为虚拟,但无论在运行时实际使用Base
的哪个子类,编译器都必须使用相同的返回类型。这就是协变返回类型不能完全随意的原因。也就是说,您不能在基类中返回int
,在派生类中返回string
。派生类的返回类型必须是基类中返回的类型的子类型。
这里可能值得注意的是,虽然你能够转换返回类型并且所有内容都正确编译,但你也可以使用指针并获得正确的值:
Derived* derived = static_cast< Derived* >(base)->foo( ); // OK
现在回答您的原始问题,如果您的目标是从未知类型的基指针开始并向下转换为派生指针,则需要一些运行时信息。这就是RTTI所做的,使用与vtable一起存储的信息来确定实际类型。如果您没有RTTI,但是您有一些嵌入式枚举,那么您当然可以使用它。我看不到你的课程,但是这样的事情会起作用:
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_cast
从int*
投射到string*
。 reinterpret_cast
允许这样做,但是你只是强迫可能是坏事并且你不想这样做。