Object Layout in Case of Virtual Functions and Multiple Inheritance

Object layout in case of virtual functions and multiple inheritance

The memory layout and the vtable layout depend on your compiler. Using my gcc for instance, they look like this:

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20

Note that sizeof(int) and the space needed for the vtable pointer can also vary from compiler to compiler and platform to platform. The reason why sizeof(C) == 20 and not 16 is that gcc gives it 8 bytes for the A subobject, 8 bytes for the B subobject and 4 bytes for its member int c.


Vtable for C
C::_ZTV1C: 6u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI1C)
8 A::funA
12 (int (*)(...))-0x00000000000000008
16 (int (*)(...))(& _ZTI1C)
20 B::funB

Class C
size=20 align=4
base size=20 base align=4
C (0x40bd5e00) 0
vptr=((& C::_ZTV1C) + 8u)
A (0x40bd6080) 0
primary-for C (0x40bd5e00)
B (0x40bd60c0) 8
vptr=((& C::_ZTV1C) + 20u)

Using virtual inheritance

class C : public virtual A, public virtual B

the layout changes to


Vtable for C
C::_ZTV1C: 12u entries
0 16u
4 8u
8 (int (*)(...))0
12 (int (*)(...))(& _ZTI1C)
16 0u
20 (int (*)(...))-0x00000000000000008
24 (int (*)(...))(& _ZTI1C)
28 A::funA
32 0u
36 (int (*)(...))-0x00000000000000010
40 (int (*)(...))(& _ZTI1C)
44 B::funB

VTT for C
C::_ZTT1C: 3u entries
0 ((& C::_ZTV1C) + 16u)
4 ((& C::_ZTV1C) + 28u)
8 ((& C::_ZTV1C) + 44u)

Class C
size=24 align=4
base size=8 base align=4
C (0x40bd5e00) 0
vptridx=0u vptr=((& C::_ZTV1C) + 16u)
A (0x40bd6080) 8 virtual
vptridx=4u vbaseoffset=-0x0000000000000000c vptr=((& C::_ZTV1C) + 28u)
B (0x40bd60c0) 16 virtual
vptridx=8u vbaseoffset=-0x00000000000000010 vptr=((& C::_ZTV1C) + 44u)

Using gcc, you can add -fdump-class-hierarchy to obtain this information.

Virtual tables and memory layout in multiple virtual inheritance

Virtual bases are very different from ordinary bases. Remember that "virtual" means "determined at runtime" -- thus the entire base subobject must be determined at runtime.

Imagine that you are getting a B & x reference, and you are tasked to find the A::a member. If the inheritance were real, then B has a superclass A, and thus the B-object which you are viewing through x has an A-subobject in which you can locate your member A::a. If the most-derived object of x has multiple bases of type A, then you can only see that particular copy which is the subobject of B.

But if the inheritance is virtual, none of this makes sense. We don't know which A-subobject we need -- this information simply doesn't exist at compile time. We could be dealing with an actual B-object as in B y; B & x = y;, or with a C-object like C z; B & x = z;, or something entirely different that derives virtually from A many more times. The only way to know is to find the actual base A at runtime.

This can be implemented with one more level of runtime indirection. (Note how this is entirely parallel to how virtual functions are implemented with one extra level of runtime indirection compared to non-virtual functions.) Instead of having a pointer to a vtable or base subobject, one solution is to store a pointer to a pointer to the actual base subobject. This is sometimes called a "thunk" or "trampoline".

So the actual object C z; may look as follows. The actual ordering in memory is up to the compiler and unimportant, and I've suppressed vtables.

+-+------++-+------++-----++-----+
|T| B1 ||T| B2 || C || A |
+-+------++-+------++-----++-----+
| | |
V V ^
| | +-Thunk-+ |
+--->>----+-->>---| ->>-+
+-------+

Thus, no matter whether you have a B1& or a B2&, you first look up the thunk, and that one in turn tells you where to find the actual base subobject. This also explains why you cannot perform a static cast from an A& to any of the derived types: this information simply doesn't exist at compile time.

For a more in-depth explanation, take a look at this fine article. (In that description, the thunk is part of the vtable of C, and virtual inheritance always necessitates the maintenance of vtables, even if there are no virtual functions anywhere.)

memory layout of a multiple-inherited object in C++

The use of virtual is redundant in D1.

From C++11, §10.3¶2:

If a virtual member function vf is declared in a class Base and in a class Derived, derived directly or indirectly from Base, a member function vf with the same name, parameter-type-list (8.3.5), cv-qualification, and ref-qualifier (or absence of same) as Base::vf is declared, then Derived::vf is also virtual (whether or not it is so declared) and it overrides111 Base::vf.



111) A function with the same name but a different parameter list (Clause 13) as a virtual function is not necessarily virtual and does not override. The use of the virtual specifier in the declaration of an overriding function is legal but redundant (has empty semantics). Access control (Clause 11) is not considered in determining overriding.

Thus, the memory layout (which is what the question seems to be about) is the same for D and D1. Obviously, different types will have different virtual tables.

C++ Memory Layout: Questions about multiple inheritance, virtual destructors, and virtual function tables

  1. What is the meaning of the content in the red box?
  2. Does the above relate to "C++ trunk"?

The symbols you highlighed are mangled, you can use some demangle tools, like c++filt

> c++filt _ZThn16_N6Derive6f_b2_1Ev
non-virtual thunk to Derive::f_b2_1()

As to your rest questions, you could refer to What is a 'thunk'? this SO question and answers.

It's a part of ABI, you could refer to Itanium C++ ABI

How does vtable handle multiple inheritance?

The class will have 2 pointers to vtables, one to its implementation of Genius and one to its implementation of CoolDude. When casting to a base class, the the returned pointer will differ from the original by the offset of the vtable(and other members) or the base class.



Related Topics



Leave a reply



Submit