虚拟多重继承和类型转换

3
我尝试创建一个继承自多个类的类,如下所示,得到了一个“菱形”(D继承自B和C。B和C都虚拟继承自A):
A / \ B C \ / D
现在,我有一个容器,其中包含一个链表,存储指向基类(A)的指针。 当我尝试对一个指针进行显式转换(在typeid检查后),我得到了以下错误: "无法将指向基类“A”的指针转换为指向派生类“D”的指针——基类是虚拟的" 但是,当我使用动态转换时,似乎一切正常。有人可以解释一下为什么我必须使用动态转换以及为什么虚拟继承会导致这个错误吗?

6
考虑修正你的设计。 :) - user1804599
1
@rightfold 每次我看到关于指针转换等的帖子时,人们都建议修复设计。但是如果我想使用通用容器,我该怎么做呢? - David Tzoor
1
@David Tzoor,让你的基类提供一个方便的抽象接口,由派生类来实现。这样你就不需要知道具体的类型了。 - Mark B
1
@DavidTzoor 问题不在指针的转换或包含上。问题在于可怕的死亡之钻本身。 (在理想情况下,继承根本不可见,只是实现细节。) - user1804599
1
@rightfold 自从钻石可以成为“可怕的钻石”?我以为钻石是珍贵的。 - curiousguy
显示剩余4条评论
2个回答

4
"虚拟"总是意味着“在运行时确定”。虚函数在运行时定位,虚基类也在运行时定位。虚拟性的整个重点在于无法静态地确定实际目标。
因此,在编译时不可能确定你所给出的虚基类的最派生对象,因为基类和最派生对象之间的关系并不固定。你必须等到知道实际对象是什么才能决定它与基类的关系。这就是动态转换所做的事情。"

但是,当我确定对象的类型后,为什么我不能“强制”显式转换呢?在typeid检查之后进行显式转换和dynamic_cast之间有什么区别?@Kerrek SB - David Tzoor
@DavidTzoor 请看这里:https://dev59.com/hWUo5IYBdhLWcg3wmAWi - Johan
2
@DavidTzoor:再说一遍,无论你了解类型多少,你都不知道结构。想象一下,在你的例子中,你想将A*强制转换为B*。但是要做到这一点,您必须知道对象实际上是一个B还是一个D!每种情况的答案都不同。虚基类不属于B,而属于最派生的对象。 - Kerrek SB

2
当我尝试对指针进行显式转换(在typeid检查之后)时,
成功执行 typeid(x)== typeid(T)后,您就知道了 x 的动态类型,并且理论上可以避免与 dynamic_cast 有关的任何其他运行时检查。另一方面,编译器不需要执行这种静态分析。 static_cast (x)不能向编译器传递 x 的动态类型确实是 T 的知识:前提条件较弱(即 T 对象具有 x 作为子对象基类)。
C ++可以提供 static_exact_cast (x),仅当 x 指定动态类型 T 的对象时才有效(而不像 static_cast 那样派生自 T 的某些类型)。这个假想的 static_exact_cast (x)通过假设 x 的动态类型为 T ,将跳过任何运行时检查并根据对 T 对象布局的了解计算正确的地址:因为在
D d;
B &br = d;

不需要运行时偏移计算,在 static_exact_cast<D&>(br) 中,反向调整不涉及任何运行时偏移计算。

鉴于

B &D_to_B (D &dr) {
  return dr;
}

需要在 D_to_B 中进行运行时偏移计算(除非整个程序分析显示没有任何类继承自 D 并且具有不同的基类 A 偏移量);给定:
struct E1 : virtual A
struct E2 : virtual A
struct F : E1, D, E2
D子对象在F中的布局将与D完整对象的布局不同: A子对象将具有不同的偏移量。 D_to_B所需的偏移量将由D的vtable(或直接存储在对象中)给出;这意味着D_to_B不仅涉及常量偏移量,而是一个简单的“静态”向上转换(在进入对象的构造函数之前,vptr尚未设置,因此此类转换无法工作;在构造函数初始化列表中小心使用向上转换)。

顺便说一下,D_to_B(d)static_cast<B&>(d)之间没有区别,因此您可以看到在static_cast内部可以执行偏移量的运行时计算。

考虑以下代码编译原始方式(假设没有复杂的分析显示ar具有动态类型F):

F f;
D &dr = f; // static offset
A &ar = dr; // runtime offset
D &dr2 = dynamic_cast<D&>(ar);

从对未知动态类型的 lvalue D 的引用中找到 A 基类主题需要运行时检查 vtable(或等效物)。返回到 D 子对象需要进行非平凡计算:
- 使用 A 的 vtable 找出完整对象的地址(该对象恰好为 F 类型) - 再次使用 vtable 找出 f 的不明确和公开派生的 D 基类子对象的地址
这不是一件简单的事情,因为 dynamic_cast(ar) 在静态上无法了解 F 的任何特定信息(F 的布局、F 的 vtable 的布局),而所有东西都从 vtable 中获取。dynamic_cast 只知道有一个 A 的派生类,并且 vtable 具有所有信息。
当然,在 C++ 中没有 static_exact_cast<>,因此必须使用 dynamic_cast 进行运行时检查;dynamic_cast 是一个复杂的函数,但复杂性覆盖了基类情况;当将动态类型给 dynamic_cast 时,避免了基类树遍历,并且测试相当简单。
结论:
要么在 dynamic_cast 中命名动态类型,这样 dynamic_cast 仍然很快且简单,要么不命名,而 dynamic_cast 的复杂性确实是必需的。

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