Hello! Welcome back to our deep dive into C++ internals.
In our last lesson, we explored object slicing, seeing how copying a derived object into a base class variable strips away its polymorphic identity. This reinforced a crucial principle: to preserve polymorphism, we must use pointers or references. This understanding of how a derived object contains a base subobject is the perfect foundation for today's topic.
We are now moving into a more complex area of object-oriented design: multiple inheritance. This is where a single class can inherit from two or more base classes. Our goal is to explain the memory layout with multiple inheritance and the associated complexities, such as the presence of multiple vptrs. We will dissect how the compiler arranges these objects in memory and solves the challenges that arise, particularly with this pointer management.
1. The Memory Layout of Multiple Base Classes
Let's start with a straightforward case: a class C that inherits from two unrelated base classes, A and B, both of which have virtual functions.
class A {
public:
virtual void funcA() { /* ... */ }
// ... members of A
};
class B {
public:
virtual void funcB() { /* ... */ }
// ... members of B
};
class C : public A, public B {
public:
// ... members of C
};
Since a C object "is an" A and "is a" B, the compiler constructs the memory for a C object by simply placing the subobjects for its base classes one after another. The layout will look like this:
- The
Asubobject (containingA'svptrand data members). - The
Bsubobject (containingB'svptrand data members). C's own data members.
Notice the immediate consequence: an object of type C now contains two virtual pointers (vptr)—one for each base class that has a vtable.
To see a detailed walkthrough of this layout and the construction process, let's watch a segment from a CppCon talk.
C++ Under the Hood: Internal Class Mechanisms - Chris Ryan - CppCon 2024
This clip from the talk 'C++ Under the Hood: Internal Class Mechanisms' by Chris Ryan clearly illustrates the memory footprint of an object with multiple inheritance.
Please watch the segment from 33:21 to 38:48. Focus on the diagrams showing the memory layout for 'C inherits from A and B'. Pay close attention to how the compiler handles the two different 'this' pointers depending on whether you're working with an A*, a B*, or a C*.
2. The this Pointer and Thunks
As the video explained, the existence of multiple subobjects introduces a crucial complexity: pointer adjustment.
Consider an object c_obj of type C:
C c_obj;
C* c_ptr = &c_obj;
- A pointer to its
Asubobject (A* a_ptr = c_ptr;) will have the same address asc_ptr, becauseAis the first base class and is located at the beginning of theCobject. - However, a pointer to its
Bsubobject (B* b_ptr = c_ptr;) will have a different address. The compiler automatically adds an offset toc_ptrto point to the beginning of theBsubobject insidec_obj.
This is straightforward for data access, but it creates a challenge for virtual function calls. Imagine C overrides a virtual function from B:
class C : public A, public B {
public:
void funcB() override { /* ... uses members of C ... */ }
int c_data;
};
When you call b_ptr->funcB(), the vtable lookup directs the call to C::funcB(). However, the this pointer inside C::funcB() must point to the beginning of the entire C object so it can access c_data and any members from A, not just the B subobject that b_ptr points to.
How does the compiler fix this? It uses a clever mechanism called a thunk (or trampoline). The vtable entry for funcB within the B subobject of C doesn't point directly to C::funcB. Instead, it points to a tiny piece of auto-generated code—the thunk—whose only job is to adjust the this pointer (by subtracting the offset) and then jump to the actual C::funcB implementation.
For a deep dive into how this is implemented at the assembly level, the following resources are excellent. Your background in C and memory will make the assembly and diagrams here very insightful.
The 'this' pointer in multiple inheritance of a C++ class
This Stack Overflow answer provides a fantastic, detailed look at how multiple inheritance and 'this' pointer adjustment are handled, complete with diagrams and GCC assembly.
Please read the answers by Ben Voigt and Anita Zeng. Ben Voigt's answer introduces the concept of a 'trampoline' (thunk). Anita Zeng's answer provides a brilliant, detailed breakdown with assembly code, showing exactly how the pointers are laid out and how the thunk works to adjust the 'rdi' register (which holds the 'this' pointer) before jumping to the real function.
To reinforce this, let's look at another resource that provides clear ASCII-art diagrams of the vtables and the thunk mechanism.
VTable Notes on Multiple Inheritance in GCC C++ ...
The article 'VTable Notes on Multiple Inheritance in GCC C++' gives another perspective on the same topic, with diagrams that clearly show the two vtables within one object and explain the role of thunks.
Read the first part of the page, down to just before the 'The Diamond' section. Focus on the diagrams illustrating the object layout for 'class C : public A, public B'. Note how it explains pointer correction for the 'this' pointer and shows the GDB disassembly of a thunk.
The image below shows a more complex but realistic example of an object layout with multiple inheritance, clearly labeling the multiple vptrs and the presence of thunks in the vtables.
.png)
Test your understanding!
Given the following code:
struct Base1 { int x = 1; virtual ~Base1() = default; };
struct Base2 { int y = 2; virtual ~Base2() = default; };
struct Derived : public Base1, public Base2 { int z = 3; };
int main() {
Derived* d = new Derived();
Base1* b1 = d;
Base2* b2 = d;
std::cout << "Address of Derived object: " << d << std::endl;
std::cout << "Address pointed to by b1: " << b1 << std::endl;
std::cout << "Address pointed to by b2: " << b2 << std::endl;
delete d;
}
What do you expect the output to be regarding the pointer addresses? Will d, b1, and b2 all hold the same value? Explain why or why not based on the object memory layout.
Show answer
You should expect d and b1 to have the same address, while b2 will have a different (higher) address.
Explanation:
The Derived object is laid out in memory with the Base1 subobject first, followed by the Base2 subobject.
dpoints to the beginning of the completeDerivedobject.b1is a pointer to the first base class,Base1. Since theBase1subobject is at the very beginning of theDerivedobject, no pointer adjustment is needed. Thus,b1will have the same address asd.b2is a pointer to the second base class,Base2. The compiler must adjust the pointer to point to the beginning of theBase2subobject, which is located after theBase1subobject in memory. Therefore,b2will have a different, larger address thand.
The output will look something like this (exact addresses will vary):
Address of Derived object: 0x55a4...
Address pointed to by b1: 0x55a4...
Address pointed to by b2: 0x55a4... + offset (e.g., 0x55a4...8 on a 64-bit system where Base1 has a vptr)
3. A Preview of the "Diamond Problem"
This exploration of multiple inheritance naturally leads us to a famous C++ conundrum: the diamond problem. This occurs when a class inherits from two other classes that both share a common base class.

What happens to the memory layout of CatDog in this scenario? Without any special handling, a CatDog object would end up containing two separate Animal subobjects: one via Cat and one via Dog. This leads to ambiguity and often isn't the desired behavior.
The following video clip introduces this problem, setting the stage for the solution we will discuss in our next lesson.
CppCon 2017: Arthur O'Dwyer “dynamic_cast From Scratch”
In his talk 'dynamic_cast From Scratch', Arthur O'Dwyer gives an excellent graphical explanation of the diamond problem and the memory duplication it causes.
Please watch from 04:18 to 06:04. Notice how the memory layout for CatDog contains two Animal subobjects. This is the core of the diamond problem that we will solve next.
Conclusion
Today we've peeled back another layer of C++'s object model, revealing the mechanics of multiple inheritance. You are now equipped with a low-level understanding that many C++ programmers lack.
Key Takeaways:
- An object with multiple base classes contains the subobjects of its bases laid out sequentially in memory.
- If multiple base classes have virtual functions, the derived object will contain multiple
vptrs, one for each base subobject with a vtable. - Casting a derived pointer to a non-primary base class pointer requires the compiler to perform pointer adjustment.
- When a virtual function from a non-primary base is overridden, the compiler generates a thunk to adjust the
thispointer back to the start of the full derived object before making the call. - This structure can lead to the diamond problem, where a common base class is duplicated in the final object's memory layout.
In our next lesson, we will tackle the diamond problem head-on by exploring virtual inheritance, the C++ feature designed to solve it. We will analyze its implementation and the significant memory overhead and complexity it introduces.