当涉及虚继承时,为什么不能使用static_cast进行向下转型?

54
请提供需要翻译的完整内容,我才能开始翻译。
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使代码有效。

这个规则存在的技术原因是什么?


我希望你对我的编辑感到满意。 - Lightness Races in Orbit
6个回答

45

技术问题在于无法从 Base* 推算出 Base 子对象的起始位置与 Derived 对象的起始位置之间的偏移量。

在您的示例中,看起来没问题,因为只有一个类涉及到具有 Base 基类,所以继承被视为不相关。但编译器无法知道是否定义了另一个 class Derived2 : public virtual Base, public Derived {} 并将指向该对象的 Base* 进行转换。总的来说[*],Derived2Base 子对象和 Derived 子对象之间的偏移量可能与最派生类型为 Derived 的对象的完整 Derived 对象之间 Base 子对象和 Derived 子对象之间的偏移量不同,这正是由于 Base 是虚继承的缘故。

因此,无法确定完整对象的动态类型,并且根据该动态类型,给定的转换指针和所需结果之间的偏移量可能不同。因此该转换是不可能的。

您的 Base 没有虚函数,因此也没有 RTTI,因此肯定无法确定完整对象的类型。即使 Base 有 RTTI(我不知道为什么),该转换仍然被禁止,但我猜在这种情况下检查一下是否可以进行 dynamic_cast

[*] 意思是,如果这个例子不能证明这一点,请继续添加更多的虚继承,直到找到一个偏移量不同的情况为止 ;-)


5
我头有点疼,所以我不能说这是一个完全清晰的答案,但在我看来它似乎是正确的。 :) - Lightness Races in Orbit
@Tomalak:是的,需要比我手头上有的时间更多的时间,而且还要在午餐时间去商店。我必须编写一些具有虚拟继承的类,并确定它们的布局以确定差异,而不是手摆那部分... - Steve Jessop
1
@Steve 我知道清楚地解释实际问题的唯一方法是在 DerivedDerived2 的情况下绘制(典型)布局图。而在 ASCII 中绘制这样的图表很痛苦。 - James Kanze
一般情况下,能进行 dynamic_cast 类型转换吗? - Aaron McDaid
2
这个解释不够清晰,能否请您再详细说明一下? - Hemant Bhargava
显示剩余3条评论

22

static_cast 只能在编译时期已知类之间的内存布局进行转换。而 dynamic_cast 可以在运行时检查信息,从而更准确地检查转换的正确性,并读取有关内存布局的运行时信息。

虚拟继承将一些运行时信息放入每个对象中,指明了 BaseDerived 之间的内存布局是什么。是一个紧接着另一个还是有额外的空隙?由于 static_cast 无法访问这些信息,编译器会保守处理并给出编译错误。


更详细地说:

考虑一个复杂的继承结构,在多重继承中因为有多个 Base 的副本而出现。最典型的情况是钻石继承:

class Base {...};
class Left : public Base {...};
class Right : public Base {...};
class Bottom : public Left, public Right {...};
在这种情况下,BottomLeftRight 组成,每个都有自己的 Base 副本。所有上述类的内存结构在编译时已知,可以毫无问题地使用 static_cast
现在让我们考虑具有虚拟继承的 Base 的类似结构:
class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};

使用虚拟继承可以确保在创建Bottom时,它仅包含一个Base的实例,该实例在对象部分LeftRight之间是共享的。例如,Bottom对象的布局可以如下所示:

Base part
Left part
Right part
Bottom part

现在,考虑将Bottom转换为Right(这是有效的转换)。您获得一个Right指针,指向由两部分构成的对象:BaseRight之间有一个内存间隙,其中包含(现在不相关的)Left部分。关于此间隙的信息存储在Right的隐藏字段中(通常称为vbase_offset)。例如,您可以在此处阅读详细信息。

但是,如果您只创建一个独立的Right对象,则不会存在间隙。

因此,如果我只给您一个Right指针,您无法在编译时知道它是独立对象还是更大的对象(例如Bottom)的一部分。您需要检查运行时信息才能正确地从Right转换为Base。这就是为什么static_cast会失败而dynamic_cast则不会。


关于dynamic_cast的说明:

虽然static_cast不使用对象的运行时信息,但dynamic_cast使用并且要求其存在!因此,后者只能用于那些包含至少一个虚函数(例如虚析构函数)的类。


5

从根本上讲,实际上没有真正的原因。但是,static_cast 的意图是非常便宜的,最多只涉及指针加或减一个常数。而且,没有办法以这种便宜的方式实现您想要的转换。基本上,由于在对象中 DerivedBase 的相对位置可能会随着另外的继承而改变,所以转换需要大量 dynamic_cast 的开销。委员会成员们可能认为,这会破坏使用 static_cast 而不是 dynamic_cast 的原因。


3
基本上,有_每个_理由。 - Lightness Races in Orbit
1
我不是投票者,但我赞同 Tomalak 的观点。在 static_cast 中的“static”表示对非空指针进行编译时转换。 - David Hammen
1
@David Hammen 这取决于您所说的“编译时”转换的含义。即使只涉及指针,static_cast 也肯定会涉及一些在运行时执行的代码。但正如我所说,代码相当有限:从指针中添加或减去一个常量,但不查找 vtable 中的内容。(我不确定为什么会被踩,因为我的回答完全正确。但是这里的投票似乎非常随机; 我认为它们没有任何意义。) - James Kanze
3
@JamesKanze的意思与您的大致相同。会有运行时代码,但其性质是受限的:检查空指针、添加在编译时确定的固定偏移量。静态转换在没有运行时类型信息(RTTI)的情况下也可以使用。通常情况下,动态转换不能使用(存在某些重叠;动态转换可用于向上转换)。 - David Hammen
@David 这符合我理解的委员会意图,尽管在当时并不存在 RTTI。大致上,对于非虚拟派生,生成的代码必须检查 null,然后加上或减去一个常量;对于虚拟派生,该常量被替换为 vtable 中的一个条目,这需要两次内存访问才能获取。在我看来,他们应该无论如何都这样做;两次内存访问并不是世界末日。但是委员会持有不同看法。 - James Kanze

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子对象(也请记住,最派生的对象调用虚基类构造函数!),实际放置位置取决于对象的“真实”类型。但由于foo只获得B的引用,任何静态转换(最多应用一些偏移量的编译时工作)都注定会失败。
对于此内容,ideone.com(http://ideone.com/2qzQu)输出如下:
0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8

1

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 的封闭对象。否则,转换的结果未定义。


2
这并不是真正的答案,因为你可以用 static_cast 进行向下转型,例如 struct B {}; struct D : B {}; int main() { B* x = new D; D* y = static_cast<D*>(x); } - Cat Plus Plus
@catPlusPlus:virtual 在哪里? - Lightness Races in Orbit
1
@eran:“Derived的内存布局在编译时已知”-虽然如此还不够(请看我的答案)。只是因为你使用了static_castDerived并不意味着所讨论的对象的最终派生类型是Derived,因此它可能没有与Derived相同的布局。这就是虚继承的含义,在从Derived派生的类中,Base子对象不是Derived子对象的一部分。 - Steve Jessop
1
提出的答案是不正确的。static_cast 不会检查有效性;例如 static_cast<Derived*>( pBase ),如果 pBase 实际上并不具有类型 Derived,则行为是未定义的。但是 static_cast 可以编译通过。 - James Kanze
@James:不,static_cast不合法 的。 - Lightness Races in Orbit
显示剩余4条评论

1

我想,这是由于具有虚拟继承的类具有不同的内存布局。父类必须在子类之间共享,因此只能连续布置其中一个子类。这意味着,您不能保证能够将连续的内存区域分离出来以将其视为派生对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接