为啥即使不涉及虚函数,虚继承也需要vtable?
Posted
技术标签:
【中文标题】为啥即使不涉及虚函数,虚继承也需要vtable?【英文标题】:Why does virtual inheritance need a vtable even if no virtual functions are involved?为什么即使不涉及虚函数,虚继承也需要vtable? 【发布时间】:2019-12-20 07:35:25 【问题描述】:我读到了这个问题:C++ Virtual class inheritance object size issue,并且想知道为什么虚拟继承会在类中产生一个额外的 vtable 指针。
我在这里找到一篇文章:https://en.wikipedia.org/wiki/Virtual_inheritance
告诉我们:
但是,在一般情况下,这个偏移量只能在运行时知道,...
我不明白这里与运行时相关的内容。完整的类继承层次结构在编译时是已知的。我了解虚函数和基指针的使用,但是虚继承没有这样的东西。
谁能解释一下为什么某些编译器(Clang/GCC)使用 vtable 实现虚拟继承以及在运行时期间如何使用它?
顺便说一句,我也看到了这个问题:vtable in case of virtual inheritance,但它只指向与虚函数相关的答案,这不是我的问题。
【问题讨论】:
注意:vtable/vptr 是实现细节。只要它们能够以某种方式实现标准要求的行为,编译器就不需要使用它们。 @RadosławCybulski:你错了,所以请点击我提供的链接。该问题明确表明,在不使用任何虚函数的情况下涉及到一个 vtable。 感谢您指出不相关的答案并将其标记为重复。问题是关于“虚拟继承”而不是“虚拟功能! @Klaus 人们有时会犯错误或感到困惑。请记住保持文明并对您的 cmets 和编辑保持耐心。 @user4581301 这个问题很好很清楚。有时人们只是碰巧问过或看到过类似的东西,所以在关闭时赶紧开枪。 【参考方案1】:完整的类继承层次结构在编译时是已知的。
确实如此;因此,如果编译器知道最派生对象的类型,那么它就知道该对象中每个子对象的偏移量。为此,不需要 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
对象,并且它不能位于与B
和C
子对象both 的通常偏移处。所以编译器必须为D
的A
子对象选择一个位置,然后它必须提供一种机制,以便一些带有B*
或C*
的代码可以找出A
子对象的位置.这仅取决于最派生类型的继承层次结构——因此 vptr/vtable 是一种合适的机制。
【讨论】:
好点!另一种“解决方案”可能是让转换函数“在某处”为每个看到的转换实现“类似”模板实例。也许更复杂和更多代码,但对象大小更小。好的,拥有一个从 vtable 中获取偏移量的转换函数是一个常用的解决方案。谢谢! @Klaus 该解决方案在不同情况下分崩离析。考虑B* b = (rand() % 2 == 0) ? new B : new D; f(b);
编译器不可能在编译时知道在每种情况下用于查找b
的A
子对象的正确偏移量。
@MilesBudnek:我会再考虑一下 :-) 谢谢!
"最多给地址加上一个静态偏移量" 稍微多一点:需要先检查是否为null。【参考方案2】:
但是,在一般情况下,这个偏移量只能在运行时知道,...
我不明白,这里与运行时相关的是什么。完整的类继承层次结构在编译时就已经知道了。
我认为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
的类定义的。因此,它只能在运行时定义。
【讨论】:
【参考方案3】:完整的类继承层次结构在编译器时是已知的。但是所有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
【讨论】:
说的很对,值得一提。不幸的是,OP 在排除有关虚拟功能的答案方面一直很直言不讳。事实上,正如其他答案所概述的那样,即使没有虚拟功能,虚拟继承也需要一个 vtable。本质原因是一样的:类的布局取决于它是否是后代类的一部分,因此是动态的。 @Maëlan 我同意你的看法。接受的答案指出,“如果编译器知道最派生对象的类型,那么它就知道该对象中每个子对象的偏移量。为此,不需要 vtable”,这是误导性的,因为编译器确实不知道也不关心a
指向的对象的类型。它只检查指针的静态类型,即A
,然后转换vptr
相关操作。不幸的是,我没有足够的声誉来发表评论
来,拿一些。 :-) 我对上述答案的理解是,一个像样的 C++ 编译器会优化操作,以便在静态知道对象的动态类型时绕过 vtable,如给定示例D d; A* a = &d;
中所示。因此,出于优化目的,它确实关心尽可能跟踪动态类型,尽管这在一般情况下当然不可行。
我会澄清自己。编译器确实需要知道d
的类型才能执行正确的向上转换A* a = &d
。但我认为这与编译器优化无关。并且在向上转换完成后,编译器生成的代码并不关心a
实际指向的内容,即它的动态类型,因为a
被视为指向A
类型的对象
This post is useful for understanding the mechanics of upcasting, e.g. A* a = &d
以上是关于为啥即使不涉及虚函数,虚继承也需要vtable?的主要内容,如果未能解决你的问题,请参考以下文章