【问题标题】: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 这个问题很好很清楚。有时人们只是碰巧问过或看到过类似的东西,所以在关闭时赶紧开枪。

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


【解决方案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 类型的对象
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-08-02
    • 2013-07-24
    • 2012-06-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多