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
使代码有效。
这个规则存在的技术原因是什么?
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
使代码有效。
这个规则存在的技术原因是什么?
技术问题在于无法从 Base*
推算出 Base
子对象的起始位置与 Derived
对象的起始位置之间的偏移量。
在您的示例中,看起来没问题,因为只有一个类涉及到具有 Base
基类,所以继承被视为不相关。但编译器无法知道是否定义了另一个 class Derived2 : public virtual Base, public Derived {}
并将指向该对象的 Base*
进行转换。总的来说[*],Derived2
中 Base
子对象和 Derived
子对象之间的偏移量可能与最派生类型为 Derived
的对象的完整 Derived
对象之间 Base
子对象和 Derived
子对象之间的偏移量不同,这正是由于 Base
是虚继承的缘故。
因此,无法确定完整对象的动态类型,并且根据该动态类型,给定的转换指针和所需结果之间的偏移量可能不同。因此该转换是不可能的。
您的 Base
没有虚函数,因此也没有 RTTI,因此肯定无法确定完整对象的类型。即使 Base
有 RTTI(我不知道为什么),该转换仍然被禁止,但我猜在这种情况下检查一下是否可以进行 dynamic_cast
。
[*] 意思是,如果这个例子不能证明这一点,请继续添加更多的虚继承,直到找到一个偏移量不同的情况为止 ;-)
Derived
和 Derived2
的情况下绘制(典型)布局图。而在 ASCII 中绘制这样的图表很痛苦。 - James Kanzedynamic_cast
类型转换吗? - Aaron McDaidstatic_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
的实例,该实例在对象部分Left
和Right
之间是共享的。例如,Bottom
对象的布局可以如下所示:
Base part
Left part
Right part
Bottom part
现在,考虑将Bottom
转换为Right
(这是有效的转换)。您获得一个Right
指针,指向由两部分构成的对象:Base
和Right
之间有一个内存间隙,其中包含(现在不相关的)Left
部分。关于此间隙的信息存储在Right
的隐藏字段中(通常称为vbase_offset
)。例如,您可以在此处阅读详细信息。
但是,如果您只创建一个独立的Right
对象,则不会存在间隙。
因此,如果我只给您一个Right
指针,您无法在编译时知道它是独立对象还是更大的对象(例如Bottom
)的一部分。您需要检查运行时信息才能正确地从Right
转换为Base
。这就是为什么static_cast
会失败而dynamic_cast
则不会。
关于dynamic_cast的说明:
虽然static_cast
不使用对象的运行时信息,但dynamic_cast
使用并且要求其存在!因此,后者只能用于那些包含至少一个虚函数(例如虚析构函数)的类。
从根本上讲,实际上没有真正的原因。但是,static_cast
的意图是非常便宜的,最多只涉及指针加或减一个常数。而且,没有办法以这种便宜的方式实现您想要的转换。基本上,由于在对象中 Derived
和 Base
的相对位置可能会随着另外的继承而改变,所以转换需要大量 dynamic_cast
的开销。委员会成员们可能认为,这会破坏使用 static_cast
而不是 dynamic_cast
的原因。
static_cast
中的“static”表示对非空指针进行编译时转换。 - David Hammenstatic_cast
也肯定会涉及一些在运行时执行的代码。但正如我所说,代码相当有限:从指针中添加或减去一个常量,但不查找 vtable
中的内容。(我不确定为什么会被踩,因为我的回答完全正确。但是这里的投票似乎非常随机; 我认为它们没有任何意义。) - James Kanzevtable
中的一个条目,这需要两次内存访问才能获取。在我看来,他们应该无论如何都这样做;两次内存访问并不是世界末日。但是委员会持有不同看法。 - James Kanzefoo
:#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);
}
0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8
static_cast
是编译时构造。它在编译时检查转换的有效性,如果转换无效,则给出一个编译错误。
virtual
是运行时现象。
两者不能同时使用。
C++03 标准 §5.2.9/2 和 §5.2.9/9 在这种情况下是相关的。
如果存在从“指向 D 的指针”到“指向 B 的指针”的有效标准转换(4.10),则类型为“指向 cv1 B 的指针”的 rvalue(其中 B 是类类型)可以转换为类型为“指向 cv2 D 的指针”的 rvalue(其中 D 是从 B 派生的类(第 10 条)),cv2 是与 cv1 相同或更大的 cv 限定符,并且 B 不是 D 的虚基类。空指针值(4.10)转换为目标类型的空指针值。如果类型为“指向 cv1 B 的指针”的 rvalue 指向实际上是类型为 D 的对象的子对象的 B,则所得到的指针指向类型为 D 的封闭对象。否则,转换的结果未定义。
static_cast
进行向下转型,例如 struct B {}; struct D : B {}; int main() { B* x = new D; D* y = static_cast<D*>(x); }
。 - Cat Plus Plusvirtual
在哪里? - Lightness Races in Orbitstatic_cast
到Derived
并不意味着所讨论的对象的最终派生类型是Derived
,因此它可能没有与Derived
相同的布局。这就是虚继承的含义,在从Derived
派生的类中,Base
子对象不是Derived
子对象的一部分。 - Steve Jessopstatic_cast
不会检查有效性;例如 static_cast<Derived*>( pBase )
,如果 pBase
实际上并不具有类型 Derived
,则行为是未定义的。但是 static_cast
可以编译通过。 - James Kanzestatic_cast
是 不合法 的。 - Lightness Races in Orbit我想,这是由于具有虚拟继承的类具有不同的内存布局。父类必须在子类之间共享,因此只能连续布置其中一个子类。这意味着,您不能保证能够将连续的内存区域分离出来以将其视为派生对象。