Skip to content

cpp vtables Part3 Virtual Inheritance

jiaxw32 edited this page Nov 5, 2023 · 1 revision

C++ vtables - Part 3 - Virtual Inheritance

原文链接

In Part 1 and Part 2 of this series we talked about how vtables work in the simplest cases, and then in multiple inheritance. Virtual inheritance complicates things even further.

在这个系列的第1部分第2部分中,我们讨论了vtable在最简单的情况下以及多重继承中的工作原理。虚拟继承进一步复杂化了事情。

As you may remember, virtual inheritance means that there’s only one instance of a base class in a concrete class. For example:

正如你可能记得的那样,虚拟继承意味着具体类中只有一个基类实例。例如:

class ios ...
class istream : virtual public ios ...
class ostream : virtual public ios ...
class iostream : public istream, public ostream

If weren’t for the virtual keyword above, iostream would in fact have two instances of ios, which may cause sync headaches and would just be inefficient.

如果不是上面的 virtual 关键字,iostream 实际上会有两个ios的实例,这可能会导致同步问题,并且效率低下。

To understand virtual inheritance we will investigate the following piece of code:

为了理解虚拟继承,我们将研究以下代码片段:

#include <iostream>
using namespace std;

class Grandparent {
 public:
  virtual void grandparent_foo() {}
  int grandparent_data;
};

class Parent1 : virtual public Grandparent {
 public:
  virtual void parent1_foo() {}
  int parent1_data;
};

class Parent2 : virtual public Grandparent {
 public:
  virtual void parent2_foo() {}
  int parent2_data;
};

class Child : public Parent1, public Parent2 {
 public:
  virtual void child_foo() {}
  int child_data;
};

int main() {
  Child child;
}

Let’s explore Child. I’ll start by dumping a whole lot of memory just where Child’s vtable begins like we did in previous posts and will then analyze the results. I suggest quickly glazing over the output here and coming back to it as I reveal details below.

让我们来探索一下 Child。我将从 Child 的虚函数表开始处转储大量内存,就像我们在之前的帖子中所做的那样,并分析结果。我建议快速浏览这里的输出,在我逐步揭示细节后再回头看它。

(gdb) p child
$1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0}
(gdb) x/600xb 0x400938
0x400938 <vtable for Child>:	0x20	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400940 <vtable for Child+8>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400948 <vtable for Child+16>:	0x00	0x0b	0x40	0x00	0x00	0x00	0x00	0x00
0x400950 <vtable for Child+24>:	0x70	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x400958 <vtable for Child+32>:	0xa0	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x400960 <vtable for Child+40>:	0x10	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400968 <vtable for Child+48>:	0xf0	0xff	0xff	0xff	0xff	0xff	0xff	0xff
0x400970 <vtable for Child+56>:	0x00	0x0b	0x40	0x00	0x00	0x00	0x00	0x00
0x400978 <vtable for Child+64>:	0x90	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x400980 <vtable for Child+72>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400988 <vtable for Child+80>:	0xe0	0xff	0xff	0xff	0xff	0xff	0xff	0xff
0x400990 <vtable for Child+88>:	0x00	0x0b	0x40	0x00	0x00	0x00	0x00	0x00
0x400998 <vtable for Child+96>:	0x80	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x4009a0 <VTT for Child>:	0x50	0x09	0x40	0x00	0x00	0x00	0x00	0x00
0x4009a8 <VTT for Child+8>:	0xf8	0x09	0x40	0x00	0x00	0x00	0x00	0x00
0x4009b0 <VTT for Child+16>:	0x18	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x4009b8 <VTT for Child+24>:	0x98	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x4009c0 <VTT for Child+32>:	0xb8	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x4009c8 <VTT for Child+40>:	0x98	0x09	0x40	0x00	0x00	0x00	0x00	0x00
0x4009d0 <VTT for Child+48>:	0x78	0x09	0x40	0x00	0x00	0x00	0x00	0x00
0x4009d8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x4009e0 <construction vtable for Parent1-in-Child>:	0x20	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x4009e8 <construction vtable for Parent1-in-Child+8>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x4009f0 <construction vtable for Parent1-in-Child+16>:	0x50	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x4009f8 <construction vtable for Parent1-in-Child+24>:	0x70	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x400a00 <construction vtable for Parent1-in-Child+32>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400a08 <construction vtable for Parent1-in-Child+40>:	0xe0	0xff	0xff	0xff	0xff	0xff	0xff	0xff
0x400a10 <construction vtable for Parent1-in-Child+48>:	0x50	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400a18 <construction vtable for Parent1-in-Child+56>:	0x80	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x400a20 <typeinfo name for Parent1>:	0x37	0x50	0x61	0x72	0x65	0x6e	0x74	0x31
0x400a28 <typeinfo name for Parent1+8>:	0x00	0x31	0x31	0x47	0x72	0x61	0x6e	0x64
0x400a30 <typeinfo name for Grandparent+7>:	0x70	0x61	0x72	0x65	0x6e	0x74	0x00	0x00
0x400a38 <typeinfo for Grandparent>:	0x50	0x10	0x60	0x00	0x00	0x00	0x00	0x00
0x400a40 <typeinfo for Grandparent+8>:	0x29	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400a48:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400a50 <typeinfo for Parent1>:	0xa0	0x10	0x60	0x00	0x00	0x00	0x00	0x00
0x400a58 <typeinfo for Parent1+8>:	0x20	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400a60 <typeinfo for Parent1+16>:	0x00	0x00	0x00	0x00	0x01	0x00	0x00	0x00
0x400a68 <typeinfo for Parent1+24>:	0x38	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400a70 <typeinfo for Parent1+32>:	0x03	0xe8	0xff	0xff	0xff	0xff	0xff	0xff
0x400a78:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400a80 <construction vtable for Parent2-in-Child>:	0x10	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400a88 <construction vtable for Parent2-in-Child+8>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400a90 <construction vtable for Parent2-in-Child+16>:	0xd0	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400a98 <construction vtable for Parent2-in-Child+24>:	0x90	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x400aa0 <construction vtable for Parent2-in-Child+32>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400aa8 <construction vtable for Parent2-in-Child+40>:	0xf0	0xff	0xff	0xff	0xff	0xff	0xff	0xff
0x400ab0 <construction vtable for Parent2-in-Child+48>:	0xd0	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400ab8 <construction vtable for Parent2-in-Child+56>:	0x80	0x08	0x40	0x00	0x00	0x00	0x00	0x00
0x400ac0 <typeinfo name for Parent2>:	0x37	0x50	0x61	0x72	0x65	0x6e	0x74	0x32
0x400ac8 <typeinfo name for Parent2+8>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400ad0 <typeinfo for Parent2>:	0xa0	0x10	0x60	0x00	0x00	0x00	0x00	0x00
0x400ad8 <typeinfo for Parent2+8>:	0xc0	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400ae0 <typeinfo for Parent2+16>:	0x00	0x00	0x00	0x00	0x01	0x00	0x00	0x00
0x400ae8 <typeinfo for Parent2+24>:	0x38	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400af0 <typeinfo for Parent2+32>:	0x03	0xe8	0xff	0xff	0xff	0xff	0xff	0xff
0x400af8 <typeinfo name for Child>:	0x35	0x43	0x68	0x69	0x6c	0x64	0x00	0x00
0x400b00 <typeinfo for Child>:	0xa0	0x10	0x60	0x00	0x00	0x00	0x00	0x00
0x400b08 <typeinfo for Child+8>:	0xf8	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400b10 <typeinfo for Child+16>:	0x02	0x00	0x00	0x00	0x02	0x00	0x00	0x00
0x400b18 <typeinfo for Child+24>:	0x50	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400b20 <typeinfo for Child+32>:	0x02	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400b28 <typeinfo for Child+40>:	0xd0	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400b30 <typeinfo for Child+48>:	0x02	0x10	0x00	0x00	0x00	0x00	0x00	0x00
0x400b38 <vtable for Grandparent>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x400b40 <vtable for Grandparent+8>:	0x38	0x0a	0x40	0x00	0x00	0x00	0x00	0x00
0x400b48 <vtable for Grandparent+16>:	0x80	0x08	0x40	0x00	0x00	0x00	0x00	0x00

Wow. That’s a lot of input. 2 new questions that immediately pop up: what’s a VTT and what’s a construction vtable for X-in-Child? We’ll answer these soon enough.

哇,这是很多输入。有两个新问题立刻浮现出来:什么是 VTT 和什么是 X-in-Child的构造vtable?我们很快就会回答这些问题。

Let’s start with Child’s memory layout:

让我们从 Child 类的内存布局开始

Size Value
8 bytes _vptr$Parent1
4 bytes parent1_data (+ 4 bytes padding)
8 bytes _vptr$Parent2
4 bytes parent2_data
4 bytes child_data
8 bytes _vptr$Grandparent
4 bytes grandparent_data (+ 4 bytes padding)

Indeed, Child has only 1 instance of Grandparent. The non-trivial thing is that it is last in memory even though it is topmost in the hierarchy.

确实,Child 只有一个 Grandparent 的实例。非常重要的一点是,尽管它在层次结构中处于最高位置,但它却位于内存中的最后位置。

Here’s the vtable layout:

以下是vtable布局:

Address Value Meaning
0x400938 0x20 (32) virtual-base offset (we’ll discuss this soon)
0x400940 0 top_offset
0x400948 0x400b00 typeinfo for Child
0x400950 0x400870 Parent1::parent1_foo(). Parent1’s vtable pointer points here.
0x400958 0x4008a0 Child::child_foo()
0x400960 0x10 (16) virtual-base offset
0x400968 -16 top_offset
0x400970 0x400b00 typeinfo for Child
0x400978 0x400890 Parent2::parent2_foo(). Parent2’s vtable pointer points here.
0x400980 0 virtual-base offset
0x400988 -32 top_offset
0x400990 0x400b00 typeinfo for Child
0x400998 0x400880 Grandparent::grandparent_foo(). Grandparent’s vtable pointer points here.

Above there’s a new concept - virtual-base offset. We’ll soon understand why it’s there.

上面有一个新概念 - 虚基偏移。我们很快就会明白它为什么存在。

Let’s further explore these weird-looking construction tables. Here’s construction vtable for Parent1-in-Child:

让我们进一步探索这些看起来奇怪的构造表。这是 Parent1-in-Child 的构造 vtable

Value Meaning
0x20 (32) virtual-base offset
0 top-offset
0x400a50 typeinfo for Parent1
0x400870 Parent1::parent1_foo()
0 virtual-base offset
-32 top-offset
0x400a50 typeinfo for Parent1
0x400880 Grandparent::grandparent_foo()

At this point I think it would be clearer to describe the process rather than dump more tables with random numbers on you. So here goes:

在这一点上,我认为描述过程比向您倾泻更多带有随机数字的表格要会更清晰。所以让我们开始吧:

Imagine you’re Child. You are asked to construct yourself on a fresh new piece of memory. Since you’re inheriting Grandparent directly (that’s what virtual-inheritance means), first you will call its constructor directly (if it wasn’t virtual inheritance you’d call Parent1’s constructor, which in turn would have called Grandparent’s constructor). You set this += 32 bytes, as this is where Grandparent’s data sits, and you call the constructor. Easy peasy.

想象一下你是 Child。你被要求在一块新的内存中构建自己。由于你直接继承了 Grandparent(这就是虚继承的意思),首先你将直接调用它的构造函数(如果不是虚继承的话,你将调用 Parent1 的构造函数,然后再调用 Grandparent 的构造函数)。你设置 this += 32 bytes,因为这是 Grandparent 的数据所在的位置,然后调用构造函数。非常简单。

Next it’s time to construct Parent1. Parent1 can safely assume that by the time it constructs itself Grandparent had already been constructed, so it can, for instance, access Grandparent’s data and methods. But wait, how can it know where to find this data? It’s not even near Parent1’s variables!

接下来是构造 Parent1 的时候了。Parent1 可以安全地假设在它构造自己的时候,Grandparent 已经被构造完毕,因此它可以访问 Grandparent 的数据和方法。但是等等,它怎么知道在哪里找到这些数据呢?这些数据甚至不在 Parent1 的变量附近!

Enters construction table for Parent1-in-Child. This table is dedicated to telling Parent1 where to find the pieces of data it can access. this is pointing to the Parent1’s data. virtual-base offset tells it where it can find Grandparent’s data: Jump 32 bytes ahead of this and you’ll find Grandparent’s memory. Get it? virtual-base offset is similar to top_offset but for virtual classes.

进入 Child 中的 Parent1 的构造表。这个表专门用来告诉 Parent1 可以访问到的数据的位置。this 指向了 Parent1 的数据。virtual-base offset 告诉 Parent1 在哪里可以找到 Grandparent 的数据:跳过 this 的前32字节,你就可以找到 Grandparent 的内存。明白了吗?virtual-base offset 类似于 top_offset,但用于虚继承的类。

Now that we understand this, constructing Parent2 is basically the same, only using construction table for Parent2-in-Child. And indeed, Parent2-in-Child has a virtual-base offset of 16 bytes.

现在我们明白了这一点,构建 Parent2 基本上与之前的构建过程相同,只不过使用的是 Child 中的 Parent2 的构造表。而且,确实可以看到 Parent2-in-Childvirtual-base offset 是 16 字节。

Take a moment to let all this info sink in. Are you ready to continue? Good.

请花点时间来消化这些信息。你准备好继续了吗?很好。

Now let’s get back to that VTT thingy. Here’s the VTT layout:

现在让我们回到那个 VTT 的事情。以下是 VTT 布局:

Address Value Symbol Meaning
0x4009a0 0x400950 vtable for Child + 24 Parent1’s entries in Child’s vtable
0x4009a8 0x4009f8 construction vtable for Parent1-in-Child + 24 Parent1’s methods in Parent1-in-Child
0x4009b0 0x400a18 construction vtable for Parent1-in-Child + 56 Grandparent's methods for Parent1-in-Child
0x4009b8 0x400a98 construction vtable for Parent2-in-Child + 24 Parent2's methods in Parent2-in-Child
0x4009c0 0x400ab8 construction vtable for Parent2-in-Child + 56 Grandparent’s methods for Parent2-in-Child
0x4009c8 0x400998 vtable for Child + 96 Grandparent’s entries in Child’s vtable
0x4009d0 0x400978 vtable for Child + 64 Parent2’s entries in Child’s vtable

VTT stands for virtual-table table, which means it’s a table of vtables. This is the translation table that knows if, for example, a Parent1’s constructor is called for a standalone object, for a Parent1-in-Child object, or for a Parent1-in-SomeOtherObject object. It always appears immediately after the vtable for the compiler to know where to find it, so there’s no need to keep another pointer in the objects themselves.

VTT 是虚函数表的表格,它是一张包含多个虚函数表的表格。它是一个翻译表,可以告诉编译器当调用 Parent1 的构造函数时是针对一个独立对象、一个 Parent1-in-Child 对象还是一个 Parent1-in-SomeOtherObject 对象。它总是紧随在虚函数表之后,这样编译器就知道在哪里找到它,因此不需要在对象本身中再维护另一个指针。

Pheww… many details, but I think we covered everything I wanted to cover. In Part 4 we will talk about higher level details of vtables. Don’t miss it as it’s probably the most important post in this series!

嗯...很多细节,但我认为我们已经涵盖了我想要讲述的所有内容。第四部分将讨论更高级别的vtable细节。千万不要错过,因为这可能是系列文章中最重要的一篇!

Clone this wiki locally