为什么虚继承即使没有使用虚函数也需要V表?

11
我看了这个问题:C++虚拟类继承对象大小问题,想知道为什么虚拟继承会导致类中有额外的vtable指针。
我在这里找到一篇文章:https://en.wikipedia.org/wiki/Virtual_inheritance。它告诉我们:

然而,在一般情况下,这个偏移量只有在运行时才能确定...

我不明白这里和运行时有什么关系。完整的类继承层次结构已经在编译时知道了。我理解虚拟函数和基础指针的用法,但虚拟继承中没有这样的东西。
有人可以解释一下为什么某些编译器(如Clang/GCC)采用vtable实现虚拟继承以及在运行时如何使用吗?
顺便提一下,我还看到了这个问题:虚拟继承情况下的vtable,但它只涉及与虚拟函数有关的答案,这不是我的问题。

4
注意:vtable/vptr 是实现细节。只要编译器能以某种方式实现标准所需的行为,就没有必要使用它们。 - Jesper Juhl
1
@RadosławCybulski:您错了,请按照我给出的链接进行操作。该问题明确显示涉及vtable,而没有使用任何虚函数。 - Klaus
1
感谢您指出一个无关的答案并将其标记为重复。这个问题是关于“虚拟继承”,而不是“虚拟函数”! - Klaus
2
@ Klaus :人都会犯错或者有时混淆。请您记得要保持礼貌,对评论和编辑要有耐心。 - François Andrieux
1
@FrançoisAndrieux:同意!但有时重新打开一个问题确实很难。我明确指出这不是关于“虚函数”的问题... - Klaus
显示剩余5条评论
3个回答

18
编译时已经知道完整的类继承层次结构。
如果编译器知道最派生对象的类型,则它就知道该对象中每个子对象的偏移量。对于这样的目的,不需要使用vtable。
例如,如果B和C都虚拟地派生自A,并且D从B和C派生,则在以下代码中:
D d;
A* a = &d;

D*A*的转换最多是在地址上加上一个静态偏移量。

然而,现在考虑一下这种情况:

A* f(B* b) { return b; }
A* g(C* c) { return c; }

在这里,f必须能够接受指向任何B对象的指针,包括可能是D对象的子对象或某个其他最派生类对象的B对象。在编译f时,编译器不知道B的派生类的完整集合。

如果B对象是最派生的对象,则A子对象将位于某个偏移量处。但如果B对象是D对象的一部分呢?D对象仅包含一个A对象,并且它无法从两个BC子对象的通常偏移处定位。因此,编译器必须为DA子对象选择一个位置,然后它必须提供一种机制,使得一些具有B*C*的代码可以找到A子对象的位置。这仅取决于最派生类型的继承层次结构---vptr / vtable是一种适当的机制。


好主意!另一种“解决方案”可能是在“某个地方”实现转换函数,就像为每个已见转换实例化的模板一样。这可能更复杂,需要更多代码,但对象大小会更小。好的,拥有一个单一的转换函数,它接受从虚表中的偏移量是一种常用的解决方案。谢谢! - Klaus
2
@Klaus,那个解决方案在不同情况下都会崩溃。考虑 B* b = (rand() % 2 == 0) ? new B : new D; f(b); 在每种情况下编译器都无法在编译时知道正确的偏移量以查找 bA 子对象。 - Miles Budnek
@MilesBudnek:我会再去思考一下 :-) 谢谢! - Klaus
最多,只需将静态偏移添加到地址即可。稍微复杂一些:您需要先检查是否为空。 - curiousguy

5
然而,在一般情况下,这个偏移量只有在运行时才能知道...我不明白这里和运行时有什么关系。完整的类继承层次结构在编译时已经确定。
维基百科上的链接文章提供了很好的解释和示例。
该文章中的示例代码:
struct Animal {
  virtual ~Animal() = default;
  virtual void Eat() {}
};

// Two classes virtually inheriting Animal:
struct Mammal : virtual Animal {
  virtual void Breathe() {}
};

struct WingedAnimal : virtual Animal {
  virtual void Flap() {}
};

// A bat is still a winged mammal
struct Bat : Mammal, WingedAnimal {
};

当你创建一个类型为“蝙蝠”的对象时,编译器可以选择不同的对象布局方式。

Option 2

当您创建一个类型为“蝙蝠”的对象时,编译器可以选择多种对象布局方式。
+--------------+
| Animal       |
+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| WingedAnimal |
+--------------+
| vpointer     |
| Bat          |
+--------------+

选项二

+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| WingedAnimal |
+--------------+
| vpointer     |
| Bat          |
+--------------+
| Animal       |
+--------------+

MammalWingedAnimal中包含的vpointer值定义了对Animal子对象的偏移量。这些值在运行时才能确定,因为Mammal的构造函数无法知道主题是Bat还是其他对象。如果子对象是Monkey,它将不会从WingedAnimal派生。它只是一个独立的对象。

struct Monkey : Mammal {
};

如果是这种情况,则对象的布局可能如下:

+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| Monkey       |
+--------------+
| Animal       |
+--------------+

可以看出,从Mammal子对象到Animal子对象的偏移量由派生自Mammal的类定义。因此,它只能在运行时定义。


1
完整的类继承层次结构已经在编译时知道。但是所有与vptr相关的操作,例如获取虚基类的偏移量和发出虚函数调用,都延迟到运行时进行,因为只有在运行时我们才能知道对象的实际类型。
例如,
class A() { virtual bool a() { return false; } };
class B() : public virtual A { int a() { return 0; } };
B* ptr = new B();

// assuming function a()'s index is 2 at virtual function table
// the call
ptr->a();

// will be transformed by the compiler to (*ptr->vptr[2])(ptr)
// so a right call to a() will be issued according to the type of the object ptr points to

2
这是非常正确和值得一提的。不幸的是,OP已经明确排除了关于虚函数的答案。事实上,即使没有虚函数,虚继承也需要一个vtable,正如其他答案所概述的那样。其根本原因是相同的:类的布局取决于它是否是后代类的一部分,因此是动态的。 - Maëlan
1
@Maëlan 我同意你的观点。被接受的答案中提到:“如果编译器知道最终派生对象的类型,则它知道该对象内每个子对象的偏移量。对于这样的目的,不需要vtable”,这是误导性的,因为编译器并不知道也不关心a所指向的对象的类型。它只检查指针的静态类型,即A,然后转换vptr相关操作。不幸的是,我没有足够的声望来发表评论。 - Silentroar
请拿一些 :-) 我对所述答案的理解是,一个体面的 C++ 编译器会在静态知道对象的动态类型时优化操作以绕过虚函数表,就像所给出的例子 D d; A* a = &d;。因此,它在可能的情况下确实关心跟踪动态类型,以便进行优化,尽管在一般情况下这是不可行的。 - Maëlan
我来澄清一下。编译器确实需要知道 d 的类型才能执行正确的向上转型 A* a = &d。但我认为这与编译器优化无关。在向上转型完成后,编译器生成的代码并不关心 a 实际指向什么,即它的动态类型,因为 a 被视为指向类型为 A 的对象。 - Silentroar
@curiousguy 说实话,我自己并不是 C++ 方面的专家,我只是阅读了这些答案,并且它们对我来说很有意义。现在我相当确信,在一般情况下,您确实需要 vtable 提供的信息,即使您可以在特殊情况下进行优化。任何动态内容都无法在编译时确定,这就是为什么它是动态的原因。关于 Microsoft ABI,我不知道,我只能告诉你谷歌搜索结果表明它确实存在(12 等)。 - Maëlan
显示剩余3条评论

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