虚拟多重继承和强制转换

Posted

技术标签:

【中文标题】虚拟多重继承和强制转换【英文标题】:Virtual multiple inheritance and casting 【发布时间】:2013-12-23 20:00:48 【问题描述】:

我尝试创建一个继承自多个类的类,如下所示,得到一个“钻石” (D 继承自 B 和 C。B 和 C 都继承自 A 实际上):

  A / \ B                                                 \ / D

现在,我有一个带有链表的容器,其中包含指向基类 (A) 的指针。 当我尝试对指针进行显式转换时(在 typeid 检查之后),出现以下错误: “无法将指向基类“A”的指针转换为指向派生类“D”的指针——基类是虚拟的”

但是当我使用动态转换时,它似乎工作得很好。 谁能给我解释一下为什么我必须使用动态转换以及为什么虚拟继承会导致这个错误?

【问题讨论】:

考虑修复您的设计。 :) @rightfold 每次我看到有关指针转换等的帖子时,人们都建议修复设计。但是,如果我想使用通用容器,我该怎么做呢? @David Tzoor 让您的基类提供派生类实现的方便的抽象接口。那么你就不需要知道具体的类型了。 @DavidTzoor 问题不在于指针或包含的转换。问题在于可怕的死亡钻石本身。 (在理想情况下,继承根本不可见,而只是一个实现细节。) @rightfold 从什么时候开始钻石可以成为“可怕的钻石”?我认为钻石很珍贵。 【参考方案1】:

“虚拟”始终意味着“在运行时确定”。虚拟函数位于运行时,虚拟基也位于运行时。虚拟性的全部意义在于,所讨论的实际目标是静态不可知道的。

因此,不可能在编译时确定哪个最派生对象被赋予了一个虚拟基,因为基和最派生对象之间的关系是不固定的。您必须等到知道 什么 实际对象是什么之后,才能确定它相对于基础的位置。这就是动态演员正在做的事情。

【讨论】:

但是为什么我不能在确定对象的类型之后“强制”显式转换呢? typeid 检查后的显式转换和动态转换有什么区别? @Kerrek SB @DavidTzoor 看看这里:***.com/questions/15921372/… @DavidTzoor:同样,你不知道结构,无论你对类型了解多少。假设您想在示例中将A* 转换为B*。但要做到这一点,您必须知道对象实际上是B 还是D!在每种情况下,答案都是不同的。虚拟基础不“属于”B,它属于最衍生的对象。【参考方案2】:

当我尝试对指针进行显式转换时(在 typeid 检查之后)

在成功typeid(x) == typeid(T) 之后,您知道x 的动态类型,并且理论上您可以避免此时dynamic_cast 涉及的任何其他运行时检查。 OTOH,编译器不需要做这种静态分析。

static_cast<T&>(x) 不会向编译器传达x 的动态类型确实是T 的知识:前提条件较弱(T 对象具有x 作为子对象基类)。

C++ 可以提​​供static_exact_cast<T&>(x),它仅在x 指定动态类型T 的对象时才有效(而不是从T 派生的某些类型,与static_cast<T&> 不同)。这个假设的static_exact_cast<T&>(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

FD 子对象的布局将不同于 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);

从对D(未知动态类型的左值)的引用中查找A 基类主题需要对vtable(或等效项)进行运行时检查。回到D 子对象需要不平凡的计算:

使用A的vtable找出完整对象的地址(恰好是F类型) 再次使用 vtable 找出 f 的明确且公开派生的 D 基类子对象的地址

这不是微不足道的,因为dynamic_cast&lt;D&amp;&gt;(ar) 静态地对F 一无所知(F 的布局,F 的 vtable 的布局);一切都是从 vtable 中获取的。 dynamic_cast 只知道有一个派生类 A 并且 vtable 包含所有信息。

当然,C++ 中没有static_exact_cast&lt;&gt;,所以你必须使用dynamic_cast,以及相关的运行时检查; dynamic_cast 是一个复杂的函数,但是复杂度涵盖了基类的情况;当给dynamic_cast动态类型时,避免了基类树遍历,测试也相当简单。

结论:

要么你在dynamic_cast&lt;T&gt;dynamic_cast 中命名动态类型是快速简单的,要么你不命名,dynamic_cast 的复杂性确实需要。

【讨论】:

以上是关于虚拟多重继承和强制转换的主要内容,如果未能解决你的问题,请参考以下文章

多重继承下的类型转换

多重继承下的类型转换

使用纯多态性和继承从基类调用派生类上的函数而不进行强制转换?

泛型和强制转换 - 不能将继承的类强制转换为基类

C++ 子类化、继承和强制转换

C++ 继承和强制转换