在 C++ 中,什么是虚拟基类?

发布于 2024-07-04 10:12:25 字数 221 浏览 12 评论 0原文

我想知道“虚拟基类”是什么以及它的含义。

让我举个例子:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};

I want to know what a "virtual base class" is and what it means.

Let me show an example:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

夕色琉璃 2024-07-11 10:12:25

你有点令人困惑。 我不知道你是否混淆了一些概念。

你的OP中没有虚拟基类。 你只有一个基类。

你做了虚拟继承。 这通常用在多重继承中,以便多个派生类使用基类的成员而不复制它们。

具有纯虚函数的基类不能被实例化。 这需要保罗掌握的语法。 它通常用于派生类必须定义这些函数。

我不想再解释这一点,因为我不完全明白你在问什么。

You're being a little confusing. I dont' know if you're mixing up some concepts.

You don't have a virtual base class in your OP. You just have a base class.

You did virtual inheritance. This is usually used in multiple inheritance so that multiple derived classes use the members of the base class without reproducing them.

A base class with a pure virtual function is not be instantiated. this requires the syntax that Paul gets at. It is typically used so that derived classes must define those functions.

I don't want to explain any more about this because I don't totally get what you're asking.

冷︶言冷语的世界 2024-07-11 10:12:25

这意味着对虚函数的调用将被转发到“正确的”类。

C++ FAQ Lite FTW。

简而言之,它经常用于多重继承场景,形成“钻石”层次结构。 当您调用该类中的函数并且该函数需要解析为该底层类之上的类 D1 或 D2 时,虚拟继承将打破底层类中创建的歧义。 有关图表和详细信息,请参阅常见问题解答项

它也用于姐妹代表团,这是一个强大的功能(尽管不适合胆小的人)。 请参阅常见问题解答。

另请参阅《Effective C++》第 3 版中的第 40 条(第 2 版中为 43 条)。

It means a call to a virtual function will be forwarded to the "right" class.

C++ FAQ Lite FTW.

In short, it is often used in multiple-inheritance scenarios, where a "diamond" hierarchy is formed. Virtual inheritance will then break the ambiguity created in the bottom class, when you call function in that class and the function needs to be resolved to either class D1 or D2 above that bottom class. See the FAQ item for a diagram and details.

It is also used in sister delegation, a powerful feature (though not for the faint of heart). See this FAQ.

Also see Item 40 in Effective C++ 3rd edition (43 in 2nd edition).

压抑⊿情绪 2024-07-11 10:12:25

虚拟类与虚拟继承不同。 虚拟类无法实例化,虚拟继承完全是另一回事。

维基百科对它的描述比我更好。 http://en.wikipedia.org/wiki/Virtual_inheritance

Virtual classes are not the same as virtual inheritance. Virtual classes you cannot instantiate, virtual inheritance is something else entirely.

Wikipedia describes it better than I can. http://en.wikipedia.org/wiki/Virtual_inheritance

喜爱纠缠 2024-07-11 10:12:25

虚拟继承中使用的虚拟基类是一种在使用多重继承时防止给定类的多个“实例”出现在继承层次结构中的方法。

考虑以下场景:

class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};

上面的类层次结构导致“可怕的菱形”,如下所示:

  A
 / \
B   C
 \ /
  D

D 的实例将由 B(包括 A)和 C(也包括 A)组成。因此,您有两个“实例” (为了更好的表达)A。

当你遇到这种情况时,你就有可能产生歧义。 当你这样做时会发生什么:

D d;
d.Foo(); // is this B's Foo() or C's Foo() ??

虚拟继承就是为了解决这个问题。 当您在继承类时指定 virtual 时,您就告诉编译器您只需要一个实例。

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

这意味着层次结构中仅包含 A 的一个“实例”。 因此,

D d;
d.Foo(); // no longer ambiguous

这是一个简短的摘要。 有关更多信息,请阅读此处也提供了一个很好的示例。

Virtual base classes, used in virtual inheritance, is a way of preventing multiple "instances" of a given class appearing in an inheritance hierarchy when using multiple inheritance.

Consider the following scenario:

class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};

The above class hierarchy results in the "dreaded diamond" which looks like this:

  A
 / \
B   C
 \ /
  D

An instance of D will be made up of B, which includes A, and C which also includes A. So you have two "instances" (for want of a better expression) of A.

When you have this scenario, you have the possibility of ambiguity. What happens when you do this:

D d;
d.Foo(); // is this B's Foo() or C's Foo() ??

Virtual inheritance is there to solve this problem. When you specify virtual when inheriting your classes, you're telling the compiler that you only want a single instance.

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

This means that there is only one "instance" of A included in the hierarchy. Hence

D d;
d.Foo(); // no longer ambiguous

This is a mini summary. For more information, have a read of this and this. A good example is also available here.

反话 2024-07-11 10:12:25

关于内存布局

附带说明一下,Dreaded Diamond 的问题是基类出现了多次。 因此,对于常规继承,您相信您拥有:

  A
 / \
B   C
 \ /
  D

但在内存布局中,您拥有:

A   A
|   |
B   C
 \ /
  D

这解释了为什么在调用 D::foo() 时,您会遇到歧义问题。 但当您想要使用 A 的数据成员时,真正的问题就出现了。 例如,假设我们有:

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

当您尝试从 D 访问 m_iValue 时,编译器会抗议,因为在层次结构中,它会看到两个 m_iValue,没有之一。 如果您修改其中一个,例如 B::m_iValue(即 BA::m_iValue 父级),C ::m_iValue 不会被修改(即 CA::m_iValue 父级)。

这就是虚拟继承派上用场的地方,有了它,您将回到真正的菱形布局,不仅只有一个 foo() 方法,而且只有一个 m_iValue

可能会出什么问题?

想象一下:

  • A 有一些基本特征。
  • B 向其中添加了某种很酷的数据数组(例如)
  • C 向其中添加了一些很酷的功能,例如观察者模式(例如,在 m_iValue< /代码>)。
  • D 继承自 BC,因此也继承自 A

对于正常继承,从 D 修改 m_iValue 是不明确的,必须解决这个问题。 即使是这样,D 内部也有两个 m_iValues,所以你最好记住这一点,并同时更新这两个。

使用虚拟继承,从 D 修改 m_iValue 是可以的...但是...假设您有 D。 通过其 C 接口,您附加了一个观察者。 而通过它的B接口,你更新了cool数组,这样就有直接改变m_iValue的副作用...

随着m_iValue的改变直接完成(不使用虚拟访问器方法),通过C“监听”的观察者不会被调用,因为实现监听的代码是在C中,并且B 不知道这一点...

结论

如果您的层次结构中有菱形,则意味着您有 95% 的可能性在所述层次结构中做错了事情。

About the memory layout

As a side note, the problem with the Dreaded Diamond is that the base class is present multiple times. So with regular inheritance, you believe you have:

  A
 / \
B   C
 \ /
  D

But in the memory layout, you have:

A   A
|   |
B   C
 \ /
  D

This explain why when call D::foo(), you have an ambiguity problem. But the real problem comes when you want to use a data member of A. For example, let's say we have:

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

When you'll try to access m_iValue from D, the compiler will protest, because in the hierarchy, it'll see two m_iValue, not one. And if you modify one, say, B::m_iValue (that is the A::m_iValue parent of B), C::m_iValue won't be modified (that is the A::m_iValue parent of C).

This is where virtual inheritance comes handy, as with it, you'll get back to a true diamond layout, with not only one foo() method only, but also one and only one m_iValue.

What could go wrong?

Imagine:

  • A has some basic feature.
  • B adds to it some kind of cool array of data (for example)
  • C adds to it some cool feature like an observer pattern (for example, on m_iValue).
  • D inherits from B and C, and thus from A.

With normal inheritance, modifying m_iValue from D is ambiguous and this must be resolved. Even if it is, there are two m_iValues inside D, so you'd better remember that and update the two at the same time.

With virtual inheritance, modifying m_iValue from D is ok... But... Let's say that you have D. Through its C interface, you attached an observer. And through its B interface, you update the cool array, which has the side effect of directly changing m_iValue...

As the change of m_iValue is done directly (without using a virtual accessor method), the observer "listening" through C won't be called, because the code implementing the listening is in C, and B doesn't know about it...

Conclusion

If you're having a diamond in your hierarchy, it means that you have 95% probability to have done something wrong with said hierarchy.

ぃ弥猫深巷。 2024-07-11 10:12:25

用虚拟基解释多重继承需要了解 C++ 对象模型。 最好在文章中而不是在评论框中清楚地解释该主题。

我发现解决我对这个主题的所有疑问的最好的、可读的解释是这篇文章: http://www.phpcompiler.org/articles/virtualinheritance.html

您真的不需要阅读有关该主题的任何其他内容(除非您是编译器编写者)读完之后...

Explaining multiple-inheritance with virtual bases requires a knowledge of the C++ object model. And explaining the topic clearly is best done in an article and not in a comment box.

The best, readable explanation I found that solved all my doubts on this subject was this article: http://www.phpcompiler.org/articles/virtualinheritance.html

You really won't need to read anything else on the topic (unless you are a compiler writer) after reading that...

感性 2024-07-11 10:12:25

虚拟基类是这样的类
无法实例化:你不能
用它创建直接对象。

我认为你混淆了两种截然不同的东西。 虚拟继承与抽象类不同。 虚拟继承修改函数调用的行为; 有时它会解析函数调用,否则会产生歧义,有时它会将函数调用处理推迟到非虚拟继承中期望的类之外的类。

A virtual base class is a class that
cannot be instantiated : you cannot
create direct object out of it.

I think you are confusing two very different things. Virtual inheritance is not the same thing as an abstract class. Virtual inheritance modifies the behaviour of function calls; sometimes it resolves function calls that otherwise would be ambiguous, sometimes it defers function call handling to a class other than that one would expect in a non-virtual inheritance.

无需解释 2024-07-11 10:12:25

我想补充 OJ 的善意澄清。

虚拟继承并不是没有代价的。 与所有虚拟事物一样,您的性能也会受到影响。 有一种方法可以解决这种性能问题,但可能不太优雅。

您可以向菱形添加另一层,而不是通过虚拟派生来破坏菱形,以获得如下结果:

   B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

没有一个类进行虚拟继承,所有类都公开继承。 然后,类 D21 和 D22 将隐藏虚函数 f(),这对于 DD 来说是不明确的,也许可以通过将函数声明为私有来实现。 他们分别定义一个包装函数 f1() 和 f2(),每个调用类本地(私有)f(),从而解决冲突。 如果 DD 类需要 D11::f(),则调用 f1();如果需要 D12::f(),则调用 f2()。 如果您内联定义包装器,您可能会获得大约零的开销。

当然,如果您可以更改 D11 和 D12,那么您可以在这些类中执行相同的操作,但通常情况并非如此。

I'd like to add to OJ's kind clarifications.

Virtual inheritance doesn't come without a price. Like with all things virtual, you get a performance hit. There is a way around this performance hit that is possibly less elegant.

Instead of breaking the diamond by deriving virtually, you can add another layer to the diamond, to get something like this:

   B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

None of the classes inherit virtually, all inherit publicly. Classes D21 and D22 will then hide virtual function f() which is ambiguous for DD, perhaps by declaring the function private. They'd each define a wrapper function, f1() and f2() respectively, each calling class-local (private) f(), thus resolving conflicts. Class DD calls f1() if it wants D11::f() and f2() if it wants D12::f(). If you define the wrappers inline you'll probably get about zero overhead.

Of course, if you can change D11 and D12 then you can do the same trick inside these classes, but often that is not the case.

仙气飘飘 2024-07-11 10:12:25

除了已经说过的关于多重继承和虚拟继承之外,Dobb 博士的期刊上还有一篇非常有趣的文章: 多重继承被认为有用

In addition to what has already been said about multiple and virtual inheritance(s), there is a very interesting article on Dr Dobb's Journal: Multiple Inheritance Considered Useful

泅渡 2024-07-11 10:12:25

Diamond 继承 runnable 使用示例

此示例展示了如何在典型场景中使用虚拟基类:解决 Diamond 继承问题。

考虑下面的工作示例:

main.cpp

#include <cassert>

class A {
    public:
        A(){}
        A(int i) : i(i) {}
        int i;
        virtual int f() = 0;
        virtual int g() = 0;
        virtual int h() = 0;
};

class B : public virtual A {
    public:
        B(int j) : j(j) {}
        int j;
        virtual int f() { return this->i + this->j; }
};

class C : public virtual A {
    public:
        C(int k) : k(k) {}
        int k;
        virtual int g() { return this->i + this->k; }
};

class D : public B, public C {
    public:
        D(int i, int j, int k) : A(i), B(j), C(k) {}
        virtual int h() { return this->i + this->j + this->k; }
};

int main() {
    D d = D(1, 2, 4);
    assert(d.f() == 3);
    assert(d.g() == 5);
    assert(d.h() == 7);
}

编译并运行:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

如果我们将 virtual 删除到:

class B : public virtual A

我们会得到一堵关于 GCC 无法解析通过 A 继承两次的 D 成员和方法的错误:

main.cpp:27:7: warning: virtual base ‘A’ inaccessible in ‘D’ due to ambiguity [-Wextra]
   27 | class D : public B, public C {
      |       ^
main.cpp: In member function ‘virtual int D::h()’:
main.cpp:30:40: error: request for member ‘i’ is ambiguous
   30 |         virtual int h() { return this->i + this->j + this->k; }
      |                                        ^
main.cpp:7:13: note: candidates are: ‘int A::i’
    7 |         int i;
      |             ^
main.cpp:7:13: note:                 ‘int A::i’
main.cpp: In function ‘int main()’:
main.cpp:34:20: error: invalid cast to abstract class type ‘D’
   34 |     D d = D(1, 2, 4);
      |                    ^
main.cpp:27:7: note:   because the following virtual functions are pure within ‘D’:
   27 | class D : public B, public C {
      |       ^
main.cpp:8:21: note:    ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:9:21: note:    ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:34:7: error: cannot declare variable ‘d’ to be of abstract type ‘D’
   34 |     D d = D(1, 2, 4);
      |       ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:35:14: error: request for member ‘f’ is ambiguous
   35 |     assert(d.f() == 3);
      |              ^
main.cpp:8:21: note: candidates are: ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:17:21: note:                 ‘virtual int B::f()’
   17 |         virtual int f() { return this->i + this->j; }
      |                     ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:36:14: error: request for member ‘g’ is ambiguous
   36 |     assert(d.g() == 5);
      |              ^
main.cpp:9:21: note: candidates are: ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:24:21: note:                 ‘virtual int C::g()’
   24 |         virtual int g() { return this->i + this->k; }
      |                     ^
main.cpp:9:21: note:                 ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
./main.out

在 GCC 9.3.0、Ubuntu 20.04 上测试。

Diamond inheritance runnable usage example

This example shows how to use a virtual base class in the typical scenario: to solve diamond inheritance problems.

Consider the following working example:

main.cpp

#include <cassert>

class A {
    public:
        A(){}
        A(int i) : i(i) {}
        int i;
        virtual int f() = 0;
        virtual int g() = 0;
        virtual int h() = 0;
};

class B : public virtual A {
    public:
        B(int j) : j(j) {}
        int j;
        virtual int f() { return this->i + this->j; }
};

class C : public virtual A {
    public:
        C(int k) : k(k) {}
        int k;
        virtual int g() { return this->i + this->k; }
};

class D : public B, public C {
    public:
        D(int i, int j, int k) : A(i), B(j), C(k) {}
        virtual int h() { return this->i + this->j + this->k; }
};

int main() {
    D d = D(1, 2, 4);
    assert(d.f() == 3);
    assert(d.g() == 5);
    assert(d.h() == 7);
}

Compile and run:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

If we remove the virtual into:

class B : public virtual A

we would get a wall of errors about GCC being unable to resolve D members and methods that were inherited twice via A:

main.cpp:27:7: warning: virtual base ‘A’ inaccessible in ‘D’ due to ambiguity [-Wextra]
   27 | class D : public B, public C {
      |       ^
main.cpp: In member function ‘virtual int D::h()’:
main.cpp:30:40: error: request for member ‘i’ is ambiguous
   30 |         virtual int h() { return this->i + this->j + this->k; }
      |                                        ^
main.cpp:7:13: note: candidates are: ‘int A::i’
    7 |         int i;
      |             ^
main.cpp:7:13: note:                 ‘int A::i’
main.cpp: In function ‘int main()’:
main.cpp:34:20: error: invalid cast to abstract class type ‘D’
   34 |     D d = D(1, 2, 4);
      |                    ^
main.cpp:27:7: note:   because the following virtual functions are pure within ‘D’:
   27 | class D : public B, public C {
      |       ^
main.cpp:8:21: note:    ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:9:21: note:    ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:34:7: error: cannot declare variable ‘d’ to be of abstract type ‘D’
   34 |     D d = D(1, 2, 4);
      |       ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:35:14: error: request for member ‘f’ is ambiguous
   35 |     assert(d.f() == 3);
      |              ^
main.cpp:8:21: note: candidates are: ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:17:21: note:                 ‘virtual int B::f()’
   17 |         virtual int f() { return this->i + this->j; }
      |                     ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:36:14: error: request for member ‘g’ is ambiguous
   36 |     assert(d.g() == 5);
      |              ^
main.cpp:9:21: note: candidates are: ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:24:21: note:                 ‘virtual int C::g()’
   24 |         virtual int g() { return this->i + this->k; }
      |                     ^
main.cpp:9:21: note:                 ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
./main.out

Tested on GCC 9.3.0, Ubuntu 20.04.

み格子的夏天 2024-07-11 10:12:25

常规继承

对于典型的 3 级非钻石非虚拟继承继承,当您实例化一个新的最派生对象时,将调用 new 并从堆中解析该对象所需的大小编译器的类类型并传递给 new。

new 有一个签名:

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)

并调用 malloc,返回 void 指针

这个地址然后传递给最底层派生对象的构造函数,该构造函数将立即调用中间的构造函数构造函数,然后中间构造函数将立即调用基本构造函数。 然后,基类在对象的开头存储指向其虚拟表的指针,然后在其后面存储其属性。 然后返回到中间构造函数,该构造函数将其虚拟表指针存储在同一位置,然后将其属性存储在基本构造函数存储的属性之后。 然后,它返回到最派生的构造函数,该构造函数在同一位置存储指向其虚拟表的指针,然后将其属性存储在中间构造函数存储的属性之后。

由于虚拟表指针被覆盖,因此虚拟表指针最终始终是最派生的类之一。 虚拟性向最派生的类传播,因此如果某个函数在中间类中是虚拟的,那么它在最派生的类中也将是虚拟的,但在基类中不是虚拟的。 如果您将最派生类的实例多态转换为指向基类的指针,则编译器不会将其解析为对虚拟表的间接调用,而是直接调用该函数A::function()< /代码>。 如果一个函数对于您将其转换为的类型来说是虚拟的,那么它将解析为对虚拟表的调用,该虚拟表将始终是最派生类的虚拟表。 如果该类型不是虚拟的,那么它将只调用 Type::function() 并将对象指针传递给它,并强制转换为 Type。

实际上,当我说指向其虚拟表的指针时,它实际上始终是虚拟表中的偏移量 16。

vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::CommonFunction()
        .quad   Base::VirtualFunction()

pointer is typically to the first function i.e. 

        mov     edx, OFFSET FLAT:vtable for Base+16

如果 virtual 在派生程度较低的类中是 virtual,则在派生程度较高的类中不再需要 virtual,因为它会向派生程度最高的类的方向向下传播。 但它可以用来表明该函数确实是一个虚函数,而不必检查它继承的类的类型定义。 当一个函数被声明为 virtual 时,从那时起,仅使用继承链中的最后一个实现,但在此之前,如果该对象被强制转换为该类中之前的类的类型,则该函数仍然可以非虚拟地使用。定义该方法的继承链。 在该名称和签名的方法的虚拟性开始之前,它可以在链中之前的多个类中进行非虚拟定义,并且它们在引用时将使用自己的方法(并且链中该定义之后的所有类都将使用该方法)定义(如果它们没有自己的定义),这与虚拟相反,虚拟总是使用最终定义)。 当一个方法被声明为虚拟时,它必须在该类或继承链中的更多派生类中实现,以便构建完整的对象才能使用。

override 是另一个编译器防护,它表示此函数正在覆盖某些内容,如果没有覆盖,则抛出编译器错误。

= 0 表示这是一个抽象函数

final 防止在更派生的类中再次实现虚函数,并确保最远派生类的虚表包含该类的最终函数。

= default 在文档中明确指出编译器将使用默认实现

= delete 如果尝试调用此函数,则会出现编译器错误

如果调用非虚函数,它将解析为正确的方法定义,而无需通过虚拟表。 如果您调用在继承类中具有最终定义的虚拟函数,那么如果您在调用方法时未将对象指针强制转换为该类型,它将使用其虚拟表并自动将子对象传递给它。 如果您在该类型的指针上调用在最派生类中定义的虚拟函数,那么它将使用其虚拟表,该虚拟表将是对象开头的虚拟表。 如果您在继承类型的指针上调用它,并且该函数在该类中也是虚拟的,那么它将使用该子对象的 vtable 指针,在第一个子对象的情况下,该指针将与最派生类相同的指针,它不会包含 thunk,因为对象和子对象的地址相同,因此它就像自动重铸此指针的方法一样简单,但对于第二个子对象,其 vtable 将包含一个非- virtual thunk 将继承类型的对象的指针转换为最底层派生类中的实现所期望的类型,即完整对象,因此偏移子对象指针以指向完整对象,并且在基子对象的情况下,将需要一个虚拟 thunk 将指向基类的指针偏移到完整对象,以便可以通过方法隐藏对象参数类型对其进行重铸。

使用带有引用运算符而不是通过指针(取消引用运算符)的对象会破坏多态性,并将虚拟方法视为常规方法。 这是因为由于切片,非指针类型上的多态转换不会发生。

虚拟继承

考虑

class Base
  {
      int a = 1;
      int b = 2;
  public:
      void virtual CommonFunction(){} ; //define empty method body
      void virtual VirtualFunction(){} ;
  };


class DerivedClass1: virtual public Base
  {
      int c = 3;
  public:
    void virtual DerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
  };
  
  class DerivedClass2 : virtual public Base
 {
     int d = 4;
 public:
     //void virtual DerivedCommonFunction(){} ;    
     void virtual VirtualFunction(){} ;
     void virtual DerivedCommonFunction2(){} ;
 };

class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
 {
   int e = 5;
 public:
     void virtual DerivedDerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
 };
 
 int main () {
   DerivedDerivedClass* d = new DerivedDerivedClass;
   d->VirtualFunction();
   d->DerivedCommonFunction();
   d->DerivedCommonFunction2();
   d->DerivedDerivedCommonFunction();
   ((DerivedClass2*)d)->DerivedCommonFunction2();
   ((Base*)d)->VirtualFunction();
 }

如果没有虚拟继承 bass 类,您将得到一个如下所示的对象:

而不是这样:

即,将有 2 个基础对象。

在上面的虚拟钻石继承情况中,调用new后,它将为对象分配的空间的地址传递给最底层的派生构造函数DerivedDerivedClass::DerivedDerivedClass(),它首先调用 Base::Base(),将其 vtable 写入基础的专用子对象中,然后 DerivedDerivedClass::DerivedDerivedClass() 调用 DerivedClass1::DerivedClass1 (),它将其虚拟表指针写入其子对象,并通过查询传递的 VTT 覆盖该对象末尾的基子对象指针,然后调用 DerivedClass1::DerivedClass1() 执行相同的操作,最后 DerivedDerivedClass::DerivedDerivedClass() 使用该继承类的虚拟表指针覆盖所有 3 个指针。 这不是(如上面第一张图所示) DerivedDerivedClass::DerivedDerivedClass() 调用 DerivedClass1::DerivedClass1() 和调用 Base::Base ()(覆盖虚拟指针),返回,将地址偏移到下一个子对象,调用 DerivedClass2::DerivedClass2(),然后还调用 Base::Base (),覆盖该虚拟指针,返回,然后 DerivedDerivedClass 构造函数用其虚拟表指针覆盖两个虚拟指针(在本例中,最派生的构造函数的虚拟表包含 2 个子表)共 3)。

以下全部是在调试模式 -O0 下编译的,因此会出现冗余汇编

main:
.LFB8:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     edi, 48 //pass size to new
        call    operator new(unsigned long) //call new
        mov     rbx, rax  //move the address of the allocation to rbx
        mov     rdi, rbx  //move it to rdi i.e. pass to the call
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor] //construct on this address
        mov     QWORD PTR [rbp-24], rbx  //store the address of the object on the stack as the d pointer variable on -O0, will be optimised off on -Ofast if the address of the pointer itself isn't taken in the code, because this address does not need to be on the stack, it can just be passed in a register to the subsequent methods

另外,如果代码是 DerivedDerivedClass d = DerivedDerivedClass(),则 main 函数将如下所示:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48 // make room for and zero 48 bytes on the stack for the 48 byte object, no extra padding required as the frame is 64 bytes with `rbp` and return address of the function it calls (no stack params are passed to any function it calls), hence rsp will be aligned by 16 assuming it was aligned at the start of this frame
        mov     QWORD PTR [rbp-48], 0
        mov     QWORD PTR [rbp-40], 0
        mov     QWORD PTR [rbp-32], 0
        mov     QWORD PTR [rbp-24], 0
        mov     QWORD PTR [rbp-16], 0
        mov     QWORD PTR [rbp-8], 0
        lea     rax, [rbp-48] // load the address of the cleared 48 bytes
        mov     rdi, rax // pass the address as a pointer to the 48 bytes cleared as the first parameter to the constructor
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]
        //address is not stored on the stack because the object is used directly -- there is no pointer variable -- d refers to the object on the stack as opposed to being a pointer

回到最初的示例,DerivedDerivedClass 构造函数:

DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]:
.LFB20:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
.LBB5:
        mov     rax, QWORD PTR [rbp-8] // object address now in rax 
        add     rax, 32 //increment address by 32
        mov     rdi, rax // move object address+32 to rdi i.e. pass to call
        call    Base::Base() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+8 //move address of VTT+8 to edx
        mov     rsi, rdx //pass VTT+8 address as 2nd parameter 
        mov     rdi, rax //object address as first (DerivedClass1 subobject)
        call    DerivedClass1::DerivedClass1() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        add     rax, 16  //increment object address by 16
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+24  //store address of VTT+24 in edx
        mov     rsi, rdx //pass address of VTT+24 as second parameter
        mov     rdi, rax //address of DerivedClass2 subobject as first
        call    DerivedClass2::DerivedClass2() [base object constructor]
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+24 //move this to edx
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        mov     QWORD PTR [rax], rdx. //store address of vtable for DerivedDerivedClass+24 at the start of the object
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        add     rax, 32  // increment object address by 32
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+120 //move this to edx
        mov     QWORD PTR [rax], rdx  //store vtable for DerivedDerivedClass+120 at object+32 (Base) 
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+72 //store this in edx
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     QWORD PTR [rax+16], rdx //store vtable for DerivedDerivedClass+72 at object+16 (DerivedClass2)
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+28], 5 // stores e = 5 in the object
.LBE5:
        nop
        leave
        ret

DerivedDerivedClass 构造函数使用指向对象偏移量的指针调用 Base::Base() 32. Base 将一个指向其虚拟表的指针存储在它收到的地址及其后面的成员中。

Base::Base() [base object constructor]:
.LFB11:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //stores address of object on stack (-O0)
.LBB2:
        mov     edx, OFFSET FLAT:vtable for Base+16  //puts vtable for Base+16 in edx
        mov     rax, QWORD PTR [rbp-8] //copies address of object from stack to rax
        mov     QWORD PTR [rax], rdx  //stores it address of object
        mov     rax, QWORD PTR [rbp-8] //copies address of object on stack to rax again
        mov     DWORD PTR [rax+8], 1 //stores a = 1 in the object
        mov     rax, QWORD PTR [rbp-8] //junk from -O0
        mov     DWORD PTR [rax+12], 2  //stores b = 2 in the object
.LBE2:
        nop
        pop     rbp
        ret

DerivedDerivedClass::DerivedDerivedClass() 然后使用指向对象偏移量 0 的指针调用 DerivedClass1::DerivedClass1(),并传递 DerivedDerivedClass+8 的 VTT 地址

DerivedClass1::DerivedClass1() [base object constructor]:
.LFB14:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //address of object
        mov     QWORD PTR [rbp-16], rsi  //address of VTT+8
.LBB3:
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rdx, QWORD PTR [rax]     //address of DerivedClass1-in-DerivedDerivedClass+24 now in rdx
        mov     rax, QWORD PTR [rbp-8]   //address of object now in rax
        mov     QWORD PTR [rax], rdx     //store address of DerivedClass1-in-.. in the object
        mov     rax, QWORD PTR [rbp-8]  // address of object now in rax
        mov     rax, QWORD PTR [rax]    //address of DerivedClass1-in.. now implicitly in rax
        sub     rax, 24                 //address of DerivedClass1-in-DerivedDerivedClass+0 now in rax
        mov     rax, QWORD PTR [rax]    //value of 32 now in rax
        mov     rdx, rax                // now in rdx
        mov     rax, QWORD PTR [rbp-8]  //address of object now in rax
        add     rdx, rax                //address of object+32 now in rdx
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rax, QWORD PTR [rax+8]   //derference VTT+8+8; address of DerivedClass1-in-DerivedDerivedClass+72 (Base::CommonFunction()) now in rax
        mov     QWORD PTR [rdx], rax     //store at address object+32 (offset to Base)
        mov     rax, QWORD PTR [rbp-8]  //store address of object in rax, return
        mov     DWORD PTR [rax+8], 3    //store its attribute c = 3 in the object
.LBE3:
        nop
        pop     rbp
        ret
VTT for DerivedDerivedClass:
        .quad   vtable for DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+24 //(DerivedClass1 uses this to write its vtable pointer)
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+72 //(DerivedClass1 uses this to overwrite the base vtable pointer)
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+72
        .quad   vtable for DerivedDerivedClass+120 // DerivedDerivedClass supposed to use this to overwrite Bases's vtable pointer
        .quad   vtable for DerivedDerivedClass+72 // DerivedDerivedClass supposed to use this to overwrite DerivedClass2's vtable pointer
//although DerivedDerivedClass uses vtable for DerivedDerivedClass+72 and DerivedDerivedClass+120 directly to overwrite them instead of going through the VTT

construction vtable for DerivedClass1-in-DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedClass1
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedClass1::VirtualFunction()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedClass1
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass1::VirtualFunction()
construction vtable for DerivedClass2-in-DerivedDerivedClass:
        .quad   16
        .quad   0
        .quad   typeinfo for DerivedClass2
        .quad   DerivedClass2::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -16
        .quad   0
        .quad   -16
        .quad   typeinfo for DerivedClass2
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass2::VirtualFunction()
vtable for DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedDerivedClass
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedDerivedClass::VirtualFunction()
        .quad   DerivedDerivedClass::DerivedDerivedCommonFunction()
        .quad   16
        .quad   -16
        .quad   typeinfo for DerivedDerivedClass
        .quad   non-virtual thunk to DerivedDerivedClass::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedDerivedClass
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedDerivedClass::VirtualFunction()

virtual thunk to DerivedClass1::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK0
virtual thunk to DerivedClass2::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK1
virtual thunk to DerivedDerivedClass::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK2
non-virtual thunk to DerivedDerivedClass::VirtualFunction():
        sub     rdi, 16
        jmp     .LTHUNK3

        .set    .LTHUNK0,DerivedClass1::VirtualFunction()
        .set    .LTHUNK1,DerivedClass2::VirtualFunction()
        .set    .LTHUNK2,DerivedDerivedClass::VirtualFunction()
        .set    .LTHUNK3,DerivedDerivedClass::VirtualFunction()


每个继承类都有自己的构造虚表,而最底层的派生类 DerivedDerivedClass 有一个虚表,每个类都有一个子表,它使用指向子表的指针来覆盖构造虚表继承类的构造函数为每个子对象存储的指针。 每个需要 thunk 的虚拟方法(虚拟 thunk 将对象指针从基对象偏移到对象的开头,非虚拟 thunk 将对象指针从非基对象的继承类对象偏移到对象的开头DerivedDerivedClass 类型的整个对象)。 DerivedDerivedClass 构造函数还使用虚拟表表 (VTT) 作为它需要使用的所有虚拟表指针的串行列表,并将其传递给每个构造函数(以及构造函数所在的子对象地址) for),它们用它来覆盖它们和基的 vtable 指针。

DerivedDerivedClass::DerivedDerivedClass() 然后将对象的地址+16 和 DerivedDerivedClass+2​​4 的 VTT 地址传递给 DerivedClass2::DerivedClass2() 的程序集与 DerivedClass1::DerivedClass1() 相同,但 mov DWORD PTR [rax+8], 3 行显然有 4 而不是 3 d = 4。

此后,它将对象中的所有 3 个虚拟表指针替换为指向 DerivedDerivedClass 的 vtable 中该类表示形式的偏移量的指针。

main 中对 d->VirtualFunction() 的调用:

        mov     rax, QWORD PTR [rbp-24] //store pointer to object (and hence vtable pointer) in rax
        mov     rax, QWORD PTR [rax] //dereference this pointer to vtable pointer and store virtual table pointer in rax
        add     rax, 8 // add 8 to the pointer to get the 2nd function pointer in the table
        mov     rdx, QWORD PTR [rax] //dereference this pointer to get the address of the method to call
        mov     rax, QWORD PTR [rbp-24] //restore pointer to object in rax (-O0 is inefficient, yes)
        mov     rdi, rax  //pass object to the method
        call    rdx

d->DerivedCommonFunction();:

        mov     rax, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax  //pass object to method
        call    rdx  //call the first function in the table

d->; DerivedCommonFunction2();:

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        lea     rdx, [rax+16]  //get the address of the 2nd subobject in the object
        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax+16] // get the vtable pointer of the 2nd subobject
        add     rax, 8  //call the 2nd function in this table
        mov     rax, QWORD PTR [rax]  //get the address of the 2nd function
        mov     rdi, rdx  //call it and pass the 2nd subobject to it
        call    rax

d->DerivedDerivedCommonFunction();:

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax] //get the vtable pointer
        add     rax, 16 //get the 3rd function in the first virtual table (which is where virtual functions that that first appear in the most derived class go, because they belong to the full object which uses the virtual table pointer at the start of the object)
        mov     rdx, QWORD PTR [rax] //get the address of the object
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax  //call it and pass the whole object to it
        call    rdx

((DerivedClass2*)d)->DerivedCommonFunction2();:

//it casts the object to its subobject and calls the corresponding method in its virtual table, which will be a non-virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L14
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 16
        jmp     .L15
.L14:
        mov     eax, 0
.L15:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L18
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, 16
        jmp     .L19
.L18:
        mov     edx, 0
.L19:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

((Base*)d)->VirtualFunction();:

//it casts the object to its subobject and calls the corresponding function in its virtual table, which will be a virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L20
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        sub     rax, 24
        mov     rax, QWORD PTR [rax]
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-24]
        add     rax, rdx
        jmp     .L21
.L20:
        mov     eax, 0
.L21:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L24
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        sub     rdx, 24
        mov     rdx, QWORD PTR [rdx]
        mov     rcx, rdx
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, rcx
        jmp     .L25
.L24:
        mov     edx, 0
.L25:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

Regular Inheritance

With typical 3 level non-diamond non-virtual-inheritance inheritance, when you instantiate a new most-derived-object, new is called and the size required for the object on the heap is resolved from the class type by the compiler and passed to new.

new has a signature:

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)

And makes a call to malloc, returning the void pointer

This address is then passed to the constructor of the most derived object, which will immediately call the middle constructor and then the middle constructor will immediately call the base constructor. The base then stores a pointer to its virtual table at the start of the object and then its attributes after it. This then returns to the middle constructor which will store its virtual table pointer at the same location and then its attributes after the attributes that would have been stored by the base constructor. It then returns to the most derived constructor, which stores a pointer to its virtual table at the same location and and then stores its attributes after the attributes that would have been stored by the middle constructor.

Because the virtual table pointer is overwritten, the virtual table pointer ends up always being the one of the most derived class. Virtualness propagates towards the most derived class so if a function is virtual in the middle class, it will be virtual in the most derived class but not the base class. If you polymorphically cast an instance of the most derived class to a pointer to the base class then the compiler will not resolve this to an indirect call to the virtual table and instead will call the function directly A::function(). If a function is virtual for the type you have cast it to then it will resolve to a call into the virtual table which will always be that of the most derived class. If it is not virtual for that type then it will just call Type::function() and pass the object pointer to it, cast to Type.

Actually when I say pointer to its virtual table, it's actually always an offset of 16 into the virtual table.

vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::CommonFunction()
        .quad   Base::VirtualFunction()

pointer is typically to the first function i.e. 

        mov     edx, OFFSET FLAT:vtable for Base+16

virtual is not required again in more-derived classes if it is virtual in a less-derived class because it propagates downwards in the direction of the most derived class. But it can be used to show that the function is indeed a virtual function, without having to check the classes it inherits's type definitions. When a function is declared virtual, from that point on, only the last implementation in the inheritance chain is used, but before that, it can still be used non-virtually if the object is cast to a type of a class before that in the inheritance chain that defines that method. It can be defined non-virtually in multiple classes before it in the chain before the virtualhood begins for a method of that name and signature, and they will use their own methods when referenced (and all classes after that definition in the chain will use that definition if they do not have their own definition, as opposed to virtual, which always uses the final definition). When a method is declared virtual, it must be implemented in that class or a more derived class in the inheritance chain for the full object that was constructed in order to be used.

override is another compiler guard that says that this function is overriding something and if it isn't then throw a compiler error.

= 0 means that this is an abstract function

final prevents a virtual function from being implemented again in a more derived class and will make sure that the virtual table of the most derived class contains the final function of that class.

= default makes it explicit in documentation that the compiler will use the default implementation

= delete give a compiler error if a call to this is attempted

If you call a non-virtual function, it will resolve to the correct method definition without going through the virtual table. If you call a virtual-function that has its final definition in an inherited class then it will use its virtual table and will pass the subobject to it automatically if you don't cast the object pointer to that type when calling the method. If you call a virtual function defined in the most derived class on a pointer of that type then it will use its virtual table, which will be the one at the start of the object. If you call it on a pointer of an inherited type and the function is also virtual in that class then it will use the vtable pointer of that subobject, which in the case of the first subobject will be the same pointer as the most derived class, which will not contain a thunk as the address of the object and the subobject are the same, and therefore it's just as simple as the method automatically recasting this pointer, but in the case of a 2nd sub object, its vtable will contain a non-virtual thunk to convert the pointer of the object of inherited type to the type the implementation in the most derived class expects, which is the full object, and therefore offsets the subobject pointer to point to the full object, and in the case of base subobject, will require a virtual thunk to offset the pointer to the base to the full object, such that it can be recast by the method hidden object parameter type.

Using the object with a reference operator and not through a pointer (dereference operator) breaks polymorphism and will treat virtual methods as regular methods. This is because polymorphic casting on non-pointer types can't occur due to slicing.

Virtual Inheritance

Consider

class Base
  {
      int a = 1;
      int b = 2;
  public:
      void virtual CommonFunction(){} ; //define empty method body
      void virtual VirtualFunction(){} ;
  };


class DerivedClass1: virtual public Base
  {
      int c = 3;
  public:
    void virtual DerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
  };
  
  class DerivedClass2 : virtual public Base
 {
     int d = 4;
 public:
     //void virtual DerivedCommonFunction(){} ;    
     void virtual VirtualFunction(){} ;
     void virtual DerivedCommonFunction2(){} ;
 };

class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
 {
   int e = 5;
 public:
     void virtual DerivedDerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
 };
 
 int main () {
   DerivedDerivedClass* d = new DerivedDerivedClass;
   d->VirtualFunction();
   d->DerivedCommonFunction();
   d->DerivedCommonFunction2();
   d->DerivedDerivedCommonFunction();
   ((DerivedClass2*)d)->DerivedCommonFunction2();
   ((Base*)d)->VirtualFunction();
 }

Without virtually inheriting the bass class you will get an object that looks like this:

Instead of this:

I.e. there will be 2 base objects.

In the virtual diamond inheritance situation above, after new is called, it passes the address of the allocated space for the object to the most derived constructor DerivedDerivedClass::DerivedDerivedClass(), which calls Base::Base() first, which writes its vtable in the base's dedicated subobject, it then DerivedDerivedClass::DerivedDerivedClass() calls DerivedClass1::DerivedClass1(), which writes its virtual table pointer to its subobject as well as overwriting the base subobject's pointer at the end of the object by consulting the passed VTT, and then calls DerivedClass1::DerivedClass1() to do the same, and finally DerivedDerivedClass::DerivedDerivedClass() overwrites all 3 pointers with its virtual table pointer for that inherited class. This is instead of (as illustrated in the 1st image above) DerivedDerivedClass::DerivedDerivedClass() calling DerivedClass1::DerivedClass1() and that calling Base::Base() (which overwrites the virtual pointer), returning, offsetting the address to the next subobject, calling DerivedClass2::DerivedClass2() and then that also calling Base::Base(), overwriting that virtual pointer, returning and then DerivedDerivedClass constructor overwriting both virtual pointers with its virtual table pointer (in this instance, the virtual table of the most derived constructor contains 2 subtables instead of 3).

The following is all compiled in debug mode -O0 so there will be redundant assembly

main:
.LFB8:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     edi, 48 //pass size to new
        call    operator new(unsigned long) //call new
        mov     rbx, rax  //move the address of the allocation to rbx
        mov     rdi, rbx  //move it to rdi i.e. pass to the call
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor] //construct on this address
        mov     QWORD PTR [rbp-24], rbx  //store the address of the object on the stack as the d pointer variable on -O0, will be optimised off on -Ofast if the address of the pointer itself isn't taken in the code, because this address does not need to be on the stack, it can just be passed in a register to the subsequent methods

Parenthetically, if the code were DerivedDerivedClass d = DerivedDerivedClass(), the main function would look like this:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48 // make room for and zero 48 bytes on the stack for the 48 byte object, no extra padding required as the frame is 64 bytes with `rbp` and return address of the function it calls (no stack params are passed to any function it calls), hence rsp will be aligned by 16 assuming it was aligned at the start of this frame
        mov     QWORD PTR [rbp-48], 0
        mov     QWORD PTR [rbp-40], 0
        mov     QWORD PTR [rbp-32], 0
        mov     QWORD PTR [rbp-24], 0
        mov     QWORD PTR [rbp-16], 0
        mov     QWORD PTR [rbp-8], 0
        lea     rax, [rbp-48] // load the address of the cleared 48 bytes
        mov     rdi, rax // pass the address as a pointer to the 48 bytes cleared as the first parameter to the constructor
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]
        //address is not stored on the stack because the object is used directly -- there is no pointer variable -- d refers to the object on the stack as opposed to being a pointer

Moving back to the original example, the DerivedDerivedClass constructor:

DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]:
.LFB20:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
.LBB5:
        mov     rax, QWORD PTR [rbp-8] // object address now in rax 
        add     rax, 32 //increment address by 32
        mov     rdi, rax // move object address+32 to rdi i.e. pass to call
        call    Base::Base() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+8 //move address of VTT+8 to edx
        mov     rsi, rdx //pass VTT+8 address as 2nd parameter 
        mov     rdi, rax //object address as first (DerivedClass1 subobject)
        call    DerivedClass1::DerivedClass1() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        add     rax, 16  //increment object address by 16
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+24  //store address of VTT+24 in edx
        mov     rsi, rdx //pass address of VTT+24 as second parameter
        mov     rdi, rax //address of DerivedClass2 subobject as first
        call    DerivedClass2::DerivedClass2() [base object constructor]
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+24 //move this to edx
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        mov     QWORD PTR [rax], rdx. //store address of vtable for DerivedDerivedClass+24 at the start of the object
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        add     rax, 32  // increment object address by 32
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+120 //move this to edx
        mov     QWORD PTR [rax], rdx  //store vtable for DerivedDerivedClass+120 at object+32 (Base) 
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+72 //store this in edx
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     QWORD PTR [rax+16], rdx //store vtable for DerivedDerivedClass+72 at object+16 (DerivedClass2)
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+28], 5 // stores e = 5 in the object
.LBE5:
        nop
        leave
        ret

The DerivedDerivedClass constructor calls Base::Base() with a pointer to the object offset 32. Base stores a pointer to its virtual table at the address it receives and its members after it.

Base::Base() [base object constructor]:
.LFB11:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //stores address of object on stack (-O0)
.LBB2:
        mov     edx, OFFSET FLAT:vtable for Base+16  //puts vtable for Base+16 in edx
        mov     rax, QWORD PTR [rbp-8] //copies address of object from stack to rax
        mov     QWORD PTR [rax], rdx  //stores it address of object
        mov     rax, QWORD PTR [rbp-8] //copies address of object on stack to rax again
        mov     DWORD PTR [rax+8], 1 //stores a = 1 in the object
        mov     rax, QWORD PTR [rbp-8] //junk from -O0
        mov     DWORD PTR [rax+12], 2  //stores b = 2 in the object
.LBE2:
        nop
        pop     rbp
        ret

DerivedDerivedClass::DerivedDerivedClass() then calls DerivedClass1::DerivedClass1() with a pointer to the object offset 0 and also passes the address of VTT for DerivedDerivedClass+8

DerivedClass1::DerivedClass1() [base object constructor]:
.LFB14:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //address of object
        mov     QWORD PTR [rbp-16], rsi  //address of VTT+8
.LBB3:
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rdx, QWORD PTR [rax]     //address of DerivedClass1-in-DerivedDerivedClass+24 now in rdx
        mov     rax, QWORD PTR [rbp-8]   //address of object now in rax
        mov     QWORD PTR [rax], rdx     //store address of DerivedClass1-in-.. in the object
        mov     rax, QWORD PTR [rbp-8]  // address of object now in rax
        mov     rax, QWORD PTR [rax]    //address of DerivedClass1-in.. now implicitly in rax
        sub     rax, 24                 //address of DerivedClass1-in-DerivedDerivedClass+0 now in rax
        mov     rax, QWORD PTR [rax]    //value of 32 now in rax
        mov     rdx, rax                // now in rdx
        mov     rax, QWORD PTR [rbp-8]  //address of object now in rax
        add     rdx, rax                //address of object+32 now in rdx
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rax, QWORD PTR [rax+8]   //derference VTT+8+8; address of DerivedClass1-in-DerivedDerivedClass+72 (Base::CommonFunction()) now in rax
        mov     QWORD PTR [rdx], rax     //store at address object+32 (offset to Base)
        mov     rax, QWORD PTR [rbp-8]  //store address of object in rax, return
        mov     DWORD PTR [rax+8], 3    //store its attribute c = 3 in the object
.LBE3:
        nop
        pop     rbp
        ret
VTT for DerivedDerivedClass:
        .quad   vtable for DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+24 //(DerivedClass1 uses this to write its vtable pointer)
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+72 //(DerivedClass1 uses this to overwrite the base vtable pointer)
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+72
        .quad   vtable for DerivedDerivedClass+120 // DerivedDerivedClass supposed to use this to overwrite Bases's vtable pointer
        .quad   vtable for DerivedDerivedClass+72 // DerivedDerivedClass supposed to use this to overwrite DerivedClass2's vtable pointer
//although DerivedDerivedClass uses vtable for DerivedDerivedClass+72 and DerivedDerivedClass+120 directly to overwrite them instead of going through the VTT

construction vtable for DerivedClass1-in-DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedClass1
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedClass1::VirtualFunction()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedClass1
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass1::VirtualFunction()
construction vtable for DerivedClass2-in-DerivedDerivedClass:
        .quad   16
        .quad   0
        .quad   typeinfo for DerivedClass2
        .quad   DerivedClass2::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -16
        .quad   0
        .quad   -16
        .quad   typeinfo for DerivedClass2
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass2::VirtualFunction()
vtable for DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedDerivedClass
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedDerivedClass::VirtualFunction()
        .quad   DerivedDerivedClass::DerivedDerivedCommonFunction()
        .quad   16
        .quad   -16
        .quad   typeinfo for DerivedDerivedClass
        .quad   non-virtual thunk to DerivedDerivedClass::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedDerivedClass
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedDerivedClass::VirtualFunction()

virtual thunk to DerivedClass1::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK0
virtual thunk to DerivedClass2::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK1
virtual thunk to DerivedDerivedClass::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK2
non-virtual thunk to DerivedDerivedClass::VirtualFunction():
        sub     rdi, 16
        jmp     .LTHUNK3

        .set    .LTHUNK0,DerivedClass1::VirtualFunction()
        .set    .LTHUNK1,DerivedClass2::VirtualFunction()
        .set    .LTHUNK2,DerivedDerivedClass::VirtualFunction()
        .set    .LTHUNK3,DerivedDerivedClass::VirtualFunction()


Each inherited class has its own construction virtual table and the most derived class, DerivedDerivedClass, has a virtual table with a subtable for each, and it uses the pointer to the subtable to overwrite construction vtable pointer that the inherited class's constructor stored for each subobject. Each virtual method that needs a thunk (virtual thunk offsets the object pointer from the base to the start of the object and a non-virtual thunk offsets the object pointer from an inherited class's object that isn't the base object to the start of the whole object of the type DerivedDerivedClass). The DerivedDerivedClass constructor also uses a virtual table table (VTT) as a serial list of all the virtual table pointers that it needs to use and passes it to each constructor (along with the subobject address that the constructor is for), which they use to overwrite their and the base's vtable pointer.

DerivedDerivedClass::DerivedDerivedClass() then passes the address of the object+16 and the address of VTT for DerivedDerivedClass+24 to DerivedClass2::DerivedClass2() whose assembly is identical to DerivedClass1::DerivedClass1() except for the line mov DWORD PTR [rax+8], 3 which obviously has a 4 instead of 3 for d = 4.

After this, it replaces all 3 virtual table pointers in the object with pointers to offsets in DerivedDerivedClass's vtable to the representation for that class.

The call to d->VirtualFunction() in main:

        mov     rax, QWORD PTR [rbp-24] //store pointer to object (and hence vtable pointer) in rax
        mov     rax, QWORD PTR [rax] //dereference this pointer to vtable pointer and store virtual table pointer in rax
        add     rax, 8 // add 8 to the pointer to get the 2nd function pointer in the table
        mov     rdx, QWORD PTR [rax] //dereference this pointer to get the address of the method to call
        mov     rax, QWORD PTR [rbp-24] //restore pointer to object in rax (-O0 is inefficient, yes)
        mov     rdi, rax  //pass object to the method
        call    rdx

d->DerivedCommonFunction();:

        mov     rax, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax  //pass object to method
        call    rdx  //call the first function in the table

d->DerivedCommonFunction2();:

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        lea     rdx, [rax+16]  //get the address of the 2nd subobject in the object
        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax+16] // get the vtable pointer of the 2nd subobject
        add     rax, 8  //call the 2nd function in this table
        mov     rax, QWORD PTR [rax]  //get the address of the 2nd function
        mov     rdi, rdx  //call it and pass the 2nd subobject to it
        call    rax

d->DerivedDerivedCommonFunction();:

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax] //get the vtable pointer
        add     rax, 16 //get the 3rd function in the first virtual table (which is where virtual functions that that first appear in the most derived class go, because they belong to the full object which uses the virtual table pointer at the start of the object)
        mov     rdx, QWORD PTR [rax] //get the address of the object
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax  //call it and pass the whole object to it
        call    rdx

((DerivedClass2*)d)->DerivedCommonFunction2();:

//it casts the object to its subobject and calls the corresponding method in its virtual table, which will be a non-virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L14
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 16
        jmp     .L15
.L14:
        mov     eax, 0
.L15:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L18
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, 16
        jmp     .L19
.L18:
        mov     edx, 0
.L19:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

((Base*)d)->VirtualFunction();:

//it casts the object to its subobject and calls the corresponding function in its virtual table, which will be a virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L20
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        sub     rax, 24
        mov     rax, QWORD PTR [rax]
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-24]
        add     rax, rdx
        jmp     .L21
.L20:
        mov     eax, 0
.L21:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L24
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        sub     rdx, 24
        mov     rdx, QWORD PTR [rdx]
        mov     rcx, rdx
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, rcx
        jmp     .L25
.L24:
        mov     edx, 0
.L25:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文