为什么涉及虚拟继承时不能使用static_cast向下转型?

发布于 2024-12-05 22:30:08 字数 379 浏览 1 评论 0原文

考虑以下代码:

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 virtually< /em> 继承自Base。从继承中删除virtual 会使代码有效。

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

Consider the following code:

struct Base {};
struct Derived : public virtual Base {};

void f()
{
    Base* b = new Derived;
    Derived* d = static_cast<Derived*>(b);
}

This is prohibited by the standard ([n3290: 5.2.9/2]) so the code does not compile, because Derived virtually inherits from Base. Removing the virtual from the inheritance makes the code valid.

What's the technical reason for this rule to exist?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

指尖上得阳光 2024-12-12 22:30:08

技术问题是无法从 Base* 计算出 Base 子对象的开头与 的开头之间的偏移量是多少派生对象。

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

因此,无法知道完整对象的动态类型,以及您给出的强制转换指针与所需结果之间的不同偏移量,具体取决于动态类型是什么。因此演员阵容是不可能的。

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

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

The technical problem is that there's no way to work out from a Base* what the offset is between the start of the Base sub-object and the start of the Derived object.

In your example it appears OK, because there's only one class in sight with a Base base, and so it appears irrelevant that the inheritance is virtual. But the compiler doesn't know whether someone defined another class Derived2 : public virtual Base, public Derived {}, and is casting a Base* pointing at the Base subobject of that. In general[*], the offset between the Base subobject and the Derived subobject within Derived2 might not be the same as the offset between the Base subobject and the complete Derived object of an object whose most-derived type is Derived, precisely because Base is virtually inherited.

So there's no way to know the dynamic type of the complete object, and different offsets between the pointer you've given the cast, and the required result, depending what that dynamic type is. Hence the cast is impossible.

Your Base has no virtual functions and hence no RTTI, so there certainly is no way to tell the type of the complete object. The cast is still banned even if Base does have RTTI (I don't immediately know why), but I guess without checking that a dynamic_cast is possible in that case.

[*] by which I mean, if this example doesn't prove the point then keep adding more virtual inheritance until you find a case where the offsets are different ;-)

瑶笙 2024-12-12 22:30:08

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使用并且需要它存在!因此,后一种强制转换只能用于那些至少包含一个虚拟函数(例如虚拟析构函数)的类

static_cast can perform only those casts where memory layout between the classes is known at compile-time. dynamic_cast can check information at run-time, which allows to more accurately check for cast correctness, as well as read run-time information regarding the memory layout.

Virtual inheritance puts a run-time information into each object which specifies what is the memory layout between the Base and Derived. Is one right after another or is there an additional gap? Because static_cast cannot access such information, the compiler will act conservatively and just give a compiler error.


In more detail:

Consider a complex inheritance structure, where - due to multiple inheritance - there are multiple copies of Base. The most typical scenario is a diamond inheritance:

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

In this scenario Bottom consists of Left and Right, where each has its own copy of Base. The memory structure of all the above classes is known at compile time and static_cast can be used without a problem.

Let us now consider the similar structure but with virtual inheritance of Base:

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

Using the virtual inheritance ensures that when Bottom is created, it contains only one copy of Base that is shared between object parts Left and Right. The layout of Bottom object can be for example:

Base part
Left part
Right part
Bottom part

Now, consider that you cast Bottom to Right (that is a valid cast). You obtain a Right pointer to an object that is in two pieces: Base and Right have a memory gap in between, containing the (now-irrelevant) Left part. The information about this gap is stored at run-time in a hidden field of Right (typically referred to as vbase_offset). You can read the details for example here.

However, the gap would not exist if you would just create a standalone Right object.

So, if I give you just a pointer to Right you do not know at compile time if it is a standalone object, or a part of something bigger (e.g. Bottom). You need to check the run-time information to properly cast from Right to Base. That is why static_cast will fail and dynamic_cast will not.


Note on dynamic_cast:

While static_cast does not use run-time information about the object, dynamic_cast uses and requires it to exist! Thus, the latter cast can be used only on those classes which contain at least one virtual function (e.g. a virtual destructor)

蓝戈者 2024-12-12 22:30:08

从根本上来说,没有真正的原因,但目的是
static_cast 非常便宜,最多涉及一个添加或一个
常量与指针的减法。并且没有办法
便宜地实现你想要的演员阵容;基本上,因为
对象内 DerivedBase 的相对位置可能会改变
如果有额外的继承,则转换需要良好的
处理dynamic_cast的开销;委员会成员
可能认为这违背了使用static_cast的原因
而不是dynamic_cast

Fundamentally, there's no real reason, but the intention is that
static_cast be very cheap, involving at most an addition or a
subtraction of a constant to the pointer. And there's no way to
implement the cast you want that cheaply; basically, because the
relative positions of Derived and Base within the object may change
if there is additional inheritance, the conversion would require a good
deal of the overhead of dynamic_cast; the members of the committee
probably thought that this defeats the reasons for using static_cast
instead of dynamic_cast.

尸血腥色 2024-12-12 22:30:08

考虑以下函数 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 的引用,任何 static_cast (在编译时最多通过应用一些偏移量来工作)必然会失败。

ideone.com (http://ideone.com/2qzQu) 的输出为:

0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8

Consider the following function 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);
}

Although not really portable, this function shows us the "offset" of A and B. Since the compiler can be quite liberal in placing the A subobject in case of inheritance (also remember that the most derived object calls the virtual base ctor!), the actual placement depends on the "real" type of the object. But since foo only gets a ref to B, any static_cast (which works at compile time by at most applying some offset) is bound to fail.

ideone.com (http://ideone.com/2qzQu) outputs for this:

0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8
無心 2024-12-12 22:30:08

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

虚拟主义是一种运行时现象。

两者不能在一起。

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

static_cast is a compile time construct. it checks for the validity of cast at compile time and gives an compilation error if invalid cast.

virtualism is a runtime phenomenon.

Both can't go together.

C++03 Standard §5.2.9/2 and §5.2.9/9 ar relevant in this case.

An rvalue of type “pointer to cv1 B”, where B is a class type, can be converted to an rvalue of type “pointer to cv2 D”, where D is a class derived (clause 10) from B, if a valid standard conversion from “pointer to D” to “pointer to B” exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is not a virtual base class of D. The null pointer value (4.10) is converted to the null pointer value of the destination type. If the rvalue of type “pointer to cv1 B” points to a B that is actually a sub-object of an object of type D, the resulting pointer points to the enclosing object of type D. Otherwise, the result of the cast is undefined.

ま昔日黯然 2024-12-12 22:30:08

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

I suppose, this is due to classes with virtual inheritance having different memory layout. The parent has to be shared between children, therefore only one of them could be laid out continuously. That means, you are not guaranteed to be able to separate a continuous area of memory to treat it as a derived object.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文