我读了一个问题:C++ Virtual class inheritance object size issue,想知道为什么虚拟继承会在类中导致附加的vtable指针。
我在这里找到了一篇文章:https://en.wikipedia.org/wiki/Virtual_inheritance
告诉我们:
但是,通常只能在运行时知道此偏移量,...
我在这里不了解与运行时相关的内容。完整的类继承层次结构在编译时就已经知道。我了解虚函数和基指针的用法,但是虚继承没有这种东西。
有人可以解释为什么某些编译器(Clang / GCC)使用vtable实现虚拟继承以及在运行时中如何使用它吗?
顺便说一句,我也看到了这个问题:vtable in case of virtual inheritance,但这仅指向与虚函数有关的答案,这不是我的问题。
答案 0 :(得分:15)
在编译时就已经知道完整的类继承层次结构。
真的吗?因此,如果编译器知道派生程度最高的对象的类型,则它知道该对象内每个子对象的偏移量。为此,不需要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
对象的指针,包括可能是B
对象或某些对象的子对象的D
对象的指针其他最派生的类对象。编译f
时,编译器不知道B
的完整派生类集。
如果B
对象是派生程度最高的对象,则A
子对象将位于某个偏移处。但是,如果B
对象是D
对象的一部分呢? D
对象仅包含一个A
对象,并且不能以与 both B
和C
子对象相同的偏移量定位。因此,编译器必须为A
的{{1}}子对象选择一个位置,然后它必须提供一种机制,以便某些带有D
或B*
的代码可以找到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
的对象时,编译器可以通过多种方式选择对象布局。
+--------------+
| Animal |
+--------------+
| vpointer |
| Mammal |
+--------------+
| vpointer |
| WingedAnimal |
+--------------+
| vpointer |
| Bat |
+--------------+
+--------------+
| vpointer |
| Mammal |
+--------------+
| vpointer |
| WingedAnimal |
+--------------+
| vpointer |
| Bat |
+--------------+
| Animal |
+--------------+
vpointer
和Mammal
的{{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