C++ vtables - Part 3 - Virtual Inheritance

(1763 words)

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.

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!


Comments