为什么即使不涉及虚拟功能,虚拟继承也需要vtable?

时间:2019-08-13 17:47:07

标签: c++ vtable virtual-inheritance memory-layout vptr

我读了一个问题:C++ Virtual class inheritance object size issue,想知道为什么虚拟继承会在类中导致附加的vtable指针。

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

告诉我们:

  

但是,通常只能在运行时知道此偏移量,...

我在这里不了解与运行时相关的内容。完整的类继承层次结构在编译时就已经知道。我了解虚函数和基指针的用法,但是虚继承没有这种东西。

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

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

3 个答案:

答案 0 :(得分:15)

  

在编译时就已经知道完整的类继承层次结构。

真的吗?因此,如果编译器知道派生程度最高的对象的类型,则它知道该对象内每个子对象的偏移量。为此,不需要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对象的指针,包括可能是B对象或某些对象的子对象的D对象的指针其他最派生的类对象。编译f时,编译器不知道B的完整派生类集。

如果B对象是派生程度最高的对象,则A子对象将位于某个偏移处。但是,如果B对象是D对象的一部分呢? D对象仅包含一个A对象,并且不能以与 both BC子对象相同的偏移量定位。因此,编译器必须为A的{​​{1}}子对象选择一个位置,然后它必须提供一种机制,以便某些带有DB*的代码可以找到C*子对象的位置。这完全取决于大多数派生类型的继承层次结构-因此,vptr / vtable是合适的机制。

答案 1 :(得分:3)

  
    

但是,通常只能在运行时知道此偏移量,...

  
     

我不明白这一点,这里与运行时有关。完整的类继承层次结构在编译时就已经知道。

我认为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       |
+--------------+

vpointerMammal的{​​{1}}中包含的值定义了WingedAnimal子对象的偏移量。直到运行时才能知道这些值,因为Animal的构造函数无法知道主题是Mammal还是其他对象。如果子对象是Bat,则不会从Monkey派生。只是

WingedAnimal

在这种情况下,对象布局可以是:

struct Monkey : Mammal {
};

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

答案 2 :(得分: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