请考虑以下代码:
struct Base {};
struct Derived : public virtual Base {};
void f()
{
Base* b = new Derived;
Derived* d = static_cast<Derived*>(b);
}
标准([n3290: 5.2.9/2]
)禁止这样做,因此代码无法编译,因为Derived
虚拟继承自Base
。从继承中删除virtual
会使代码有效。
此规则存在的技术原因是什么?
答案 0 :(得分:35)
技术问题是没有办法从Base*
计算出Base
子对象的开头和Derived
对象的开头之间的偏移量。
在你的例子中,它似乎没问题,因为只有一个类具有Base
基础,因此看起来与继承是虚拟的无关。但编译器不知道是否有人定义了另一个class Derived2 : public virtual Base, public Derived {}
,并且正在指向Base*
指向该Base
子对象的Base
。通常[*],Derived
子对象和Derived2
子对象之间的偏移量可能与Base
子对象和完整Derived
子对象之间的偏移量不同。 {1}}最大派生类型为Derived
的对象的对象,正是因为Base
实际上是遗传的。
因此无法知道完整对象的动态类型,以及您给出演员的指针与所需结果之间的不同偏移,具体取决于动态类型。因此演员阵容是不可能的。
你的Base
没有虚函数,因此没有RTTI,所以当然没有办法告诉整个对象的类型。即使Base
确实有RTTI(我不会立即知道原因),演员仍然被禁止,但我想如果没有检查dynamic_cast
是否可行。
[*]我的意思是,如果这个例子不能证明这一点,那么继续添加更多的虚拟继承,直到找到偏移量不同的情况; - )
答案 1 :(得分:2)
考虑以下函数foo
:
#include <iostream>
struct A
{
int Ax;
};
struct B : virtual A
{
int Bx;
};
struct C : B, virtual A
{
int Cx;
};
void foo( const B& b )
{
const B* pb = &b;
const A* pa = &b;
std::cout << (void*)pb << ", " << (void*)pa << "\n";
const char* ca = reinterpret_cast<const char*>(pa);
const char* cb = reinterpret_cast<const char*>(pb);
std::cout << "diff " << (cb-ca) << "\n";
}
int main(int argc, const char *argv[])
{
C c;
foo(c);
B b;
foo(b);
}
虽然不是真正可移植的,但是这个函数向我们展示了A和B的“偏移”。由于编译器在继承的情况下放置A子对象可以非常宽松(还记得最派生的对象调用虚拟基础ctor) !),实际放置取决于对象的“真实”类型。但是因为foo只获得了对B的引用,所以任何static_cast(在编译时通过最多应用一些偏移量)都必然会失败。
ideone.com(http://ideone.com/2qzQu)输出:
0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8
答案 2 :(得分:2)
从根本上说,没有真正的理由,但意图是这样的
static_cast
非常便宜,最多只涉及一个或一个
将常量减去指针。而且没有办法
以低廉的价格实施你想要的演员;基本上,因为
对象内Derived
和Base
的相对位置可能会发生变化
如果有额外的继承,转换将需要一个好的
处理dynamic_cast
的开销;委员会成员
可能认为这打败了使用static_cast
的原因
而不是dynamic_cast
。
答案 3 :(得分:2)
static_cast
只能执行那些在编译时已知类之间的内存布局的转换。 dynamic_cast
可以在运行时检查信息,这样可以更准确地检查转换正确性,以及读取有关内存布局的运行时信息。
虚拟继承将运行时信息放入每个对象,指定Base
和Derived
之间的内存布局。是一个接一个还是有一个额外的差距?由于static_cast
无法访问此类信息,因此编译器将采取保守措施并仅提供编译器错误。
更详细:
考虑一个复杂的继承结构,其中 - 由于多重继承 - 有Base
的多个副本。最典型的情况是钻石继承:
class Base {...};
class Left : public Base {...};
class Right : public Base {...};
class Bottom : public Left, public Right {...};
在此方案中,Bottom
由Left
和Right
组成,其中每个都有自己的Base
副本。所有上述类的内存结构在编译时都是已知的,static_cast
可以毫无问题地使用。
现在让我们考虑类似的结构,但具有Base
的虚拟继承:
class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};
使用虚拟继承可确保在创建Bottom
时,它仅包含对象部分之间共享的一个副本Base
{ {1}}和Left
。 Right
对象的布局可以是例如:
Bottom
现在,请考虑将Base part
Left part
Right part
Bottom part
投射到Bottom
(这是有效演员)。您获得了一个指向对象的Right
指针,该对象分为两部分:Right
和Base
之间存在内存间隙,包含(现在无关的)Right
部分。有关此差距的信息在运行时存储在Left
的隐藏字段中(通常称为Right
)。您可以阅读详细信息,例如here。
但是,如果您只是创建一个独立的vbase_offset
对象,则不会存在差距。
所以,如果我只给你一个指向Right
的指针,你在编译时就不知道它是一个独立的对象,还是更大的一部分(例如Right
)。您需要检查运行时信息,以便从Bottom
正确投射到Right
。这就是Base
失败而static_cast
不会失败的原因。
关于dynamic_cast的说明:
虽然dynamic_cast
不使用有关对象的运行时信息,但static_cast
使用要求存在!因此,后一个强制转换只能用于那些包含至少一个虚函数的类(例如虚拟析构函数)
答案 4 :(得分:1)
static_cast
是一个编译时构造。它在编译时检查强制转换的有效性,如果无效强制转换则给出编译错误。
virtual
ism是一种运行时现象。
两者都不能在一起。
C ++ 03标准§5.2.9/ 2和§5.2.9/ 9在这种情况下是相关的。
“指向cv1 B的指针”类型的右值,其中B是类类型,可以转换为“指向cv2 D的指针”类型的右值,其中D是从B派生的类(第10节),如果存在从“指向D的指针”到“指向B的指针”的有效标准转换(4.10),cv2与cv1相同,或者cv-qualification,cv1,和B不是虚拟基类D 。空指针值(4.10)将转换为目标类型的空指针值。如果类型“指向cv1 B的指针”的rvalue指向实际上是D类型对象的子对象的B,则生成的指针指向类型D的封闭对象。否则,转换的结果是未定义的
答案 5 :(得分:1)
我想,这是因为具有不同内存布局的虚拟继承的类。父母必须在孩子之间分享,因此其中只有一个可以连续布局。这意味着,您无法保证能够将连续的内存区域分开以将其视为派生对象。