47

考虑以下代码:

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从继承中删除使代码有效。

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

4

6 回答 6

41

技术问题是没有办法从子对象的开始和对象的开始Base*之间的偏移量来计算。BaseDerived

在您的示例中,它看起来不错,因为只有一个带有Base基类的类,因此继承是虚拟的似乎无关紧要。但是编译器不知道是否有人定义了另一个class Derived2 : public virtual Base, public Derived {},并且正在将一个Base*指向它的Base子对象。一般来说[*],Base子对象和其中的Derived子对象之间的偏移量Derived2可能与最派生类型为 的对象的Base子对象和完整对象之间的偏移量不同,正是因为它实际上是继承的。DerivedDerivedBase

所以没有办法知道完整对象的动态类型,以及你给出的指针之间的不同偏移量,以及所需的结果,这取决于动态类型是什么。因此演员阵容是不可能的。

Base没有虚函数,因此没有 RTTI,所以肯定没有办法告诉完整对象的类型。即使Base确实有 RTTI(我不立即知道为什么),演员仍然被禁止,但我想没有检查 adynamic_cast在这种情况下是可能的。

[*] 我的意思是,如果这个例子不能证明这一点,那么继续添加更多的虚拟继承,直到你发现偏移量不同的情况;-)

于 2011-09-20T12:30:05.970 回答
14

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 {...};

在这种情况下,BottomLeftand组成Right,其中每个都有自己的Base. 以上所有类的内存结构在编译时都是已知的,static_cast可以毫无问题地使用。

现在让我们考虑类似的结构,但具有虚拟继承Base

class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};

使用虚拟继承可确保在创建时,Bottom它只包含对象部分和. 对象的布局可以是例如:BaseLeftRightBottom

Base part
Left part
Right part
Bottom part

现在,考虑你投到BottomRight这是一个有效的投)。您获得一个Right指向一个对象的指针,该对象分为两部分:Base并且Right在两者之间有一个内存间隙,包含(现在不相关的)Left部分。有关此间隙的信息在运行时存储在Right(通常称为vbase_offset)的隐藏字段中。例如,您可以在此处阅读详细信息。

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

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


关于 dynamic_cast 的注意事项:

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

于 2017-06-09T21:30:31.577 回答
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
于 2011-09-20T12:48:53.743 回答
2

从根本上说,没有真正的原因,但目的是 static_cast非常便宜,最多涉及对指针的常量的加法或减法。而且没有办法以那么便宜的方式实现你想要的演员阵容;基本上,因为如果有额外的继承,对象的相对位置DerivedBase对象内的相对位置可能会发生变化,因此转换将需要大量的开销dynamic_cast;委员会成员可能认为这与使用static_cast 而不是dynamic_cast.

于 2011-09-20T12:55:10.353 回答
1

static_cast是一个编译时构造。它在编译时检查强制转换的有效性,如果强制强制转换无效,则给出编译错误。

virtual主义是一种运行时现象。

两者不能一起走。

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 限定或大于 cv1 的 cv 限定,并且 B 不是 D 的虚拟基类。空指针值 (4.10) 被转换为目标类型的空指针值。如果“指向 cv1 B 的指针”类型的右值指向实际上是 D 类型对象的子对象的 B,则生成的指针指向 D 类型的封闭对象。否则,强制转换的结果是未定义的.

于 2011-09-20T12:15:44.930 回答
1

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

于 2011-09-20T12:26:52.423 回答