C++ vtables - Part 3 - Virtual Inheritance
(1763 words) Tue, Mar 15, 2016In 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.
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.
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.
(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.
Let’s start with Child
’s memory layout:
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.
Here’s the vtable layout:
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
:
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.
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!
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.
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.
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:
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.
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!