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

时间:2011-09-20 12:10:12

标签: c++ virtual-inheritance downcast static-cast

请考虑以下代码:

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会使代码有效。

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

6 个答案:

答案 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非常便宜,最多只涉及一个或一个 将常量减去指针。而且没有办法 以低廉的价格实施你想要的演员;基本上,因为 对象内DerivedBase的相对位置可能会发生变化 如果有额外的继承,转换将需要一个好的 处理dynamic_cast的开销;委员会成员 可能认为这打败了使用static_cast的原因 而不是dynamic_cast

答案 3 :(得分:2)

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 { {1}}和LeftRight对象的布局可以是例如:

Bottom

现在,请考虑将Base part Left part Right part Bottom part 投射到Bottom(这是有效演员)。您获得了一个指向对象的Right指针,该对象分为两部分:RightBase之间存在内存间隙,包含(现在无关的)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)

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