cpp vtables Part3 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-Child
的 virtual-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细节。千万不要错过,因为这可能是系列文章中最重要的一篇!