派生 类 可以有多个指向虚拟 table 的指针吗?

Can derived classes have more than one pointer to a virtual table?

我正在观看 BackToBasics 演讲:Virtual Dispatch 及其替代方案 来自 CppCon2019。演示者说,幻灯片显示(假设我没有误解)派生的 class 从基础 class 继承了一个 vtable 指针,并且还有自己的 vptr。

当然,从技术上讲,这不是标准强制要求的,但我让自己有点困惑,我对 sizeof() 的实验似乎也暗示应该只需要一个指针。请有人澄清是否存在需要多个 vptrs 的情况?

谢谢

P.S。需要明确的是,在这种情况下,我们正在考虑更常见的 public 继承,而不是虚拟或多重继承(演示者在演讲的前面部分明确提到了这一点)。

假设我们有两个classes,每个都有至少一个虚函数,PossessionVehicle。要为其中任何一个的派生 class 的实例调用这些虚函数,需要一个指向虚拟 table 的指针。由于这两个class是独立的,所以它们的虚拟table会完全不同。

现在假设 OwnedVehicle 派生自 PossessionVehicle。要为 OwnedVehicle 的实例调用 Possession 中的虚函数,需要一个指向 Possession 所需类型的虚函数 table 的指针。同样,要为 OwnedVehicle 的实例调用 Vehicle 中的虚函数,需要指向 Vehicle.

所需类型的虚函数 table 的指针

典型的实现通过为 OwnedVehicle 构建一个虚函数 table 来处理这个问题,其中包含一部分用于 OwnedVehicle 虚函数(如果有),一部分用于 Vehicle 虚函数函数和一个用于 Possession 个虚拟函数。然后,当从指向不同类型对象的指针调用虚函数时,编译器所要做的就是将适用的增量应用到虚函数 table 指针以指向它的正确部分。

虽然多继承的情况更复杂,但单继承也是如此。 OwnedVehicle 的虚函数 table 内部包含一个 Vehicle 的虚函数 table,即使不涉及 Possession 也会这样做。

a vtable 包含 class 的每个虚函数的地址,位于已知偏移量处。

[备注:实际上与常规 class 不同,vtable 的成员位于负偏移量处,很像数组中间的指针。这只是一个约定,不会对实现自由度产生太大影响。无论如何,唯一的问题是信息在 vtable 中的放置是由约定(ABI)规定的,并且遵循相同约定的编译器会为多态 classes 生成兼容的代码。]

当派生的 class 中有附加函数时会发生什么? (不仅仅是基础 class 中的函数 "inherited")

一旦您接受了指向结构的指针既指向整个对象又指向其第一个成员的想法,您就会认为指向派生 class 的指针指向基 class 位于偏移量零处。因此,您可以拥有 完全相同的指针值,如 void* 所示,可以根据单继承约定将其用于派生对象或基类

现在您可以将其应用于任何数据结构,甚至可以应用于实际上不是 table 的 vtable(相同类型的元素数组,或可以在同样的方式)但是一个记录(不相关类型或意义的对象);你可以看到对于这样的派生 class 的 vtable 可以以完全相同的方式从其唯一基的 vtable 派生。

(注意,如果你把C++编译成C,你做这些事的时候可能会运行进入类型别名规则。当然汇编没有这个问题,也不会天真地编译"high level assembler" C。 )

所以对于单继承,基础被集成并优化到派生class:

  • 对于实例的数据成员(class 类型)
  • 对于虚函数成员,即 vtable 的数据成员(或者元 class 的成员,如果你想象的话)。

请注意,将基数置于零偏移量允许您将 vtable 基数置于零偏移量,这反过来又允许您使用相同的 vptr 但不不是暗示它;相反地​​,与基共享 vptr 意味着基 vtable 位于偏移量零(vtable layout = meta class 级别)因此基必须位于偏移量零(数据成员布局= class级)。

而多重继承实际上是单继承加上,因为一个 class 总是被视为特权:它被放置在偏移量零处所以指针是相同的,所以 vtable 可以是放置在偏移量零处(因为指针相同);其他基地,不是这样。

正如我们所见,除了一个继承的多态 classes 之外,所有的都被放置在多重继承中的非零偏移处。每个都在派生的class中携带一个额外的"inherited" vptr;该(隐藏)指针成员必须由任何派生构造函数正确填充。

这些额外的 vptr 是针对出现在非零偏移 处的基 classes,因此必须调整指向继承基的指针(添加一个正常量到转换为基指针,删除它以转换回来)。编译器需要生成代码来执行隐式转换是一个微不足道的评论(将整数转换为浮点类型是一项复杂得多的任务);但是这里 this 的转换是在给定基类型上的函数调用和登陆作为基或派生 class 中的覆盖函数的函数之间的转换:不同之处在于 调整取决于函数重写,它仅以 class(元类型的实例)为人所知。所以 vptr 需要指向不同的 vtable 信息:一个知道如何处理这些基到派生指针转换的信息。

作为"meta type" 的实例,vtables 拥有自动进行所有指针调整的所有信息。 (这些取决于所涉及的特定 class 类型,而不取决于其他因素。)

所以在实现层面,两种类型的继承是:

  • 零偏移继承;共享 vptr;在某些 vtable 和 ABI 描述中称为主要基础 class;
  • 任意偏移继承;有另一个vptr;称为二级碱基 class.

这是基本的东西。虚拟继承在实现层面要微妙得多,甚至主的概念也不是很清楚,因为虚拟基础可以是派生 class 的 "primary" 只有在一些更派生的 classes!