为什么即使不涉及虚函数,虚继承也需要虚表?

Why does virtual inheritance need a vtable even if no virtual functions are involved?

我读了这个问题:C++ Virtual class inheritance object size issue,想知道为什么虚拟继承会在 class.

中产生一个额外的 vtable 指针

我在这里找到了一篇文章:https://en.wikipedia.org/wiki/Virtual_inheritance

这告诉我们:

However this offset can in the general case only be known at runtime,...

我不明白什么是运行时相关的。完整的 class 继承层次结构在编译时已知。我了解虚函数和基指针的使用,但是虚拟继承没有这回事。

有人可以解释为什么有些编译器 (Clang/GCC) 使用 vtable 实现虚拟继承,以及它在 运行时 期间如何使用?

顺便说一句,我也看到了这个问题:vtable in case of virtual inheritance,但它只指向与虚函数相关的答案,这不是我的问题。

The complete class inheritance hierarchy is already known in compile time.

没错;因此,如果编译器知道最派生对象的类型,那么它就知道该对象中每个子对象的偏移量。为此,不需要 vtable。

例如,如果 BC 实际上都派生自 A,而 D 派生自 BC ,然后在下面的代码中:

D d;
A* a = &d;

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

但是,现在考虑这种情况:

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

此处,f 必须能够接受指向任何 B 对象的指针,包括可能是 D 对象的子对象的 B 对象或其他一些最派生的 class 对象。编译 f 时,编译器不知道 B.

的完整派生 classes

如果B对象是最派生对象,那么A子对象将位于某个偏移量处。但是如果 B 对象是 D 对象的一部分呢? D 对象只包含一个 A 对象,它不能位于 BC 的通常偏移处] 子对象。所以编译器必须为 DA 子对象选择一个位置,然后它必须提供一种机制以便某些带有 B*C* 的代码可以找到A 子对象所在的位置。这完全取决于最派生类型的继承层次结构——因此 vptr/vtable 是一种合适的机制。

However this offset can in the general case only be known at runtime,...

我不明白,运行时间在这里有什么关系。完整的 class 继承层次结构在编译时已知。

我认为 linked article at Wikipedia 通过示例提供了很好的解释。

那篇文章中的示例代码:

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 {
};

当您创建 Bat 类型的对象时,编译器可以通过多种方式选择对象布局。

选项 1

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

选项 2

+--------------+
| 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导出的classes定义的。因此,它只能在运行时间定义。

完整的 class 继承层次结构在编译器时已为人所知。但是所有 vptr 相关的操作,例如获取虚基 class 的偏移量和发出虚函数调用,都被延迟到运行时,因为只有在运行时我们才能知道对象的实际类型.

例如,

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