为什么即使不涉及虚函数,虚继承也需要虚表?
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。
例如,如果 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
对象或其他一些最派生的 class 对象。编译 f
时,编译器不知道 B
.
的完整派生 classes
如果B
对象是最派生对象,那么A
子对象将位于某个偏移量处。但是如果 B
对象是 D
对象的一部分呢? D
对象只包含一个 A
对象,它不能位于 B
和 C
的通常偏移处] 子对象。所以编译器必须为 D
的 A
子对象选择一个位置,然后它必须提供一种机制以便某些带有 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 |
+--------------+
Mammal
和 WingedAnimal
中的 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
我读了这个问题: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。
例如,如果 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
对象或其他一些最派生的 class 对象。编译 f
时,编译器不知道 B
.
如果B
对象是最派生对象,那么A
子对象将位于某个偏移量处。但是如果 B
对象是 D
对象的一部分呢? D
对象只包含一个 A
对象,它不能位于 B
和 C
的通常偏移处] 子对象。所以编译器必须为 D
的 A
子对象选择一个位置,然后它必须提供一种机制以便某些带有 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 |
+--------------+
Mammal
和 WingedAnimal
中的 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