为啥即使不涉及虚函数,虚继承也需要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。

例如,如果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 对象。编译f时,编译器并不知道B的全部派生类。

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

【讨论】:

好点!另一种“解决方案”可能是让转换函数“在某处”为每个看到的转换实现“类似”模板实例。也许更复杂和更多代码,但对象大小更小。好的,拥有一个从 vtable 中获取偏移量的转换函数是一个常用的解决方案。谢谢! @Klaus 该解决方案在不同情况下分崩离析。考虑B* b = (rand() % 2 == 0) ? new B : new D; f(b); 编译器不可能在编译时知道在每种情况下用于查找bA 子对象的正确偏移量。 @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       |
+--------------+

MammalWingedAnimal 中的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?的主要内容,如果未能解决你的问题,请参考以下文章

关于虚函数的两个问题

为啥纯虚函数初始化为0?

C++的构造函数为何不能为虚函数

C++虚函数除了vtable怎么实现? [复制]

C++性能榨汁机之虚函数的开销

缺少 vtable 通常意味着第一个非内联虚成员函数没有定义