Hello! In our previous lesson, we saw how multiple inheritance can lead to the "diamond problem," where a class like CatDog ends up with two copies of the Animal base class, causing ambiguity and memory duplication. You saw a preview of this issue from Arthur O'Dwyer's talk.
Today, we will solve this exact problem. This lesson's objective is to implement virtual inheritance to solve the 'diamond problem' and analyze its memory overhead. We'll examine the C++ keyword that enables this solution and, more importantly, dissect how it completely restructures the object's memory layout "under the hood." You'll learn why this solution, while powerful, comes with significant performance and complexity costs—a trade-off that is crucial to understand as a systems programmer.
1. The Diamond Problem in Practice
Let's start with a concrete example of the diamond problem. Imagine we're modeling entities for a game. We have a base Entity class, and from it, we derive Warrior and Goblin. Finally, we have a Player class that inherits from both Warrior and Goblin.
// Non-virtual inheritance (the problematic case)
class Entity {
public:
int x, y; // Position
};
class Warrior : public Entity { /* ... */ };
class Goblin : public Entity { /* ... */ };
class Player : public Warrior, public Goblin { /* ... */ };
int main() {
Player p;
// p.x = 10; // AMBIGUOUS! Which 'x'? Warrior's or Goblin's?
}
A Player object will contain a complete Warrior subobject (which includes an Entity) AND a complete Goblin subobject (which also includes an Entity). This results in two Entity subobjects and two sets of x and y coordinates.
To see this demonstrated with code and verified using the GDB debugger, let's watch the first part of the following video.
Classes part 24 - Multiple Inheritance Revisited (Virtual inheritance) | Modern Cpp Series Ep. 61
Mike Shah provides a clear, practical demonstration of the diamond problem, showing the compilation error due to ambiguity and then using GDB to prove that the base class members are duplicated in memory.
Watch from the beginning (00:44) to 05:56. Pay attention to how he illustrates the incorrect inheritance hierarchy versus what C++ actually does without virtual inheritance. The GDB walkthrough is particularly insightful.
2. The Solution: The virtual Inheritance Keyword
C++ provides a direct solution to this problem: the virtual keyword, used in the inheritance specification.
// Corrected with virtual inheritance
class Entity { /* ... */ };
class Warrior : public virtual Entity { /* ... */ };
class Goblin : public virtual Entity { /* ... */ };
// Player does not need 'virtual' here
class Player : public Warrior, public Goblin { /* ... */ };
int main() {
Player p;
p.x = 10; // OK! There is only one 'x'.
}
By marking the inheritance from Entity as virtual, we tell the compiler that any class further down the hierarchy (like Player) should only ever contain one single, shared instance of the Entity base class.
Let's see Mike Shah apply this fix and show how it resolves the ambiguity.
Classes part 24 - Multiple Inheritance Revisited (Virtual inheritance) | Modern Cpp Series Ep. 61
This next segment shows how to apply the 'virtual' keyword and then revisits GDB to demonstrate that there is now only one shared 'Entity' subobject.
Watch from 05:56 to 11:34. Observe how the 'virtual' keyword changes the inheritance declaration and resolves the ambiguity. The final GDB session will confirm the single, shared base subobject.
It's important to note a key detail from the video: virtual inheritance only solves ambiguity for members of the common virtual base (Entity). If Warrior and Goblin both introduce a member with the same name (e.g., power), that ambiguity will still exist and must be resolved explicitly.
3. How Virtual Inheritance is Implemented Internally
You've seen what virtual inheritance does, but your goal is to understand how. The solution is elegant conceptually but complex in its implementation, fundamentally changing the memory layout of our objects.
With non-virtual inheritance, a base class subobject has a fixed, compile-time offset within its direct child. With virtual inheritance, the single, shared Entity subobject "floats down" to the end of the most-derived object (Player).
This creates a new problem: a Warrior* pointer no longer knows the static offset to its Entity base, because that offset depends on whether the Warrior* points to a standalone Warrior or a Warrior that is part of a Player.
The solution is another level of indirection. The compiler augments the vtable to store the offset to the virtual base. When code needs to access the virtual base, it performs a runtime lookup:
- Get the object's
vptr. - Follow the
vptrto thevtable. - Look up the virtual base offset at a known location in the
vtable. - Add this offset to the current object's address to find the virtual base subobject.
Arthur O'Dwyer's talk explains this mechanism brilliantly. His explanations are based on the Itanium C++ ABI, which is used by GCC and Clang on Linux, making it directly relevant to your goals.
CppCon 2017: Arthur O'Dwyer “dynamic_cast From Scratch”
In this segment from 'dynamic_cast From Scratch', Arthur O'Dwyer explains the internal mechanics of virtual inheritance. This is the core of today's lesson.
Watch from 06:04 to 17:05. This is a dense and critical section. (06:04 - 07:03): Introduction to how virtual inheritance solves the problem conceptually. (07:03 - 12:10): The deep dive. Focus on how the memory layout changes, how the vtable is used to find the virtual base's location, and the concept of 'thunks' for adjusting the 'this' pointer when calling functions on the virtual base. (12:10 - 17:05): Analysis of the vtable structure. He shows how the 'most derived type' information and virtual base offsets are stored in the vtable, and explains the performance cost of accessing a virtual base member (the 'three dereferences' point).
4. Analyzing the Memory and Performance Overhead
As you just learned, this solution is not free. Let's explicitly analyze the overhead.
Memory Layout and Size Overhead
The memory layout becomes significantly more complex. The uj.edu.pl resource has some of the clearest diagrams available for understanding the vtable layout under the Itanium ABI.
VTable Notes on Multiple Inheritance in GCC C++ ...
This document provides incredibly detailed diagrams of the object and vtable layout with virtual inheritance.
Read the section 'The Diamond: Single Copies of Virtual Bases'. Don't worry about understanding every single detail of the diagram at first glance. The key takeaway is to see the structure: the single 'A' subobject is at the end, and the vtables for 'B' and 'C' contain 'vbase_offset' entries that point to it. This diagram is the static representation of what Arthur O'Dwyer described.
The diagram you just studied shows the vbase_offset stored directly in the vtable. This is one of the key sources of overhead. This additional information increases the size of the vtable and, consequently, the complexity of the object model.
Let's look at another visual comparison.
What is happening under the hood of virtual inheritance?
This Stack Overflow answer gives a great side-by-side visualization of the memory layout with and without virtual inheritance, making the trade-offs very clear.
Read the excellent answer by Miles Budnek. First, examine his diagram for non-virtual inheritance, confirming the two CBaseEntity sub-objects. Then, look at the diagram for virtual inheritance, noting how there is only one CBaseEntity at the end. Finally, study his last diagram showing how 'Offset Table Ptrs' are used to find the virtual bases. This is the indirection mechanism we've been discussing.
This image provides a high-quality visualization of a complete virtual inheritance layout, including the VTT (Virtual Table Table), which is used during construction.

Performance Overhead
The performance cost comes from the indirection required to access members of the virtual base.
- Non-virtual base:
ptr->memberis a compile-time calculation:address of ptr + fixed offset of member. This is typically a single machine instruction. - Virtual base: As Arthur O'Dwyer explained,
ptr->virtual_base_memberrequires runtime lookups:this -> vptr -> vtable -> vbase_offset, followed by pointer arithmetic. This can be several machine instructions and involves multiple memory reads, making it significantly slower.
Construction and Destruction Overhead
The construction and destruction process also becomes more complex. The key rule is: the most derived class is responsible for constructing all virtual bases.
When new Player is called, the Player constructor ensures the single Entity subobject is constructed exactly once. The constructors for the intermediate classes (Warrior, Goblin) are called in special "not-in-charge" versions that skip the construction of their virtual bases. This process is managed by the compiler using another complex mechanism, often involving a VTT (Virtual Table Table), which holds pointers to the correct vtables to use at each stage of construction. This is an advanced topic, but knowing it exists is part of understanding the true complexity of virtual inheritance.
Test your understanding!
A colleague is trying to optimize a piece of performance-critical code. They notice a class involved in a diamond inheritance hierarchy uses virtual inheritance. They ask you: "Why is accessing player_ptr->x potentially slower than accessing a normal member variable? Isn't it just a pointer dereference?"
How would you explain the performance difference at a low level, based on what you've learned today?
Show answer
You would explain that accessing a member of a non-virtual base class is a simple compile-time offset calculation. The compiler knows exactly where the member is relative to the start of the object.
However, because x belongs to a virtual base class, its position relative to a Warrior* or Goblin* is not fixed at compile time. Its actual location is determined by the most-derived object (Player). To find x at runtime, the program must perform an extra level of indirection:
- It follows the object's
vptrto find itsvtable. - It reads a
vbase_offsetfrom a known location within thatvtable. This offset tells it where the sharedEntitysubobject is located within the completePlayerobject. - It adds this offset to the object's pointer to find the start of the
Entitysubobject. - Finally, it applies the offset of
xwithin theEntitysubobject.
This multi-step process involving several memory reads is inherently slower than the single instruction typically needed for accessing a non-virtual member.
Conclusion
Today, you've conquered one of the most notoriously complex features in C++. You've not only learned the syntax for solving the diamond problem but have also delved into the deep ABI-level mechanics that make it work.
Key Takeaways:
- The diamond problem (ambiguity and data duplication from a common base) is solved using
public virtualinheritance. - Virtual inheritance guarantees that only one, shared subobject of the virtual base exists in the most-derived object.
- This is implemented by moving the virtual base to a variable location (often the end of the object) and using an indirection mechanism to find it at runtime.
- This mechanism typically involves storing virtual base offsets in the vtable, which are looked up when accessing the base.
- This solution has a cost:
- Memory Overhead: Objects are larger due to the need to store offset information (either in the vtable or other tables).
- Performance Overhead: Accessing members of a virtual base is slower due to the required runtime lookup. Construction and destruction are also more complex.
In our next lesson, we will continue exploring polymorphism by defining abstract base classes using pure virtual functions. This will allow us to define pure interfaces, a cornerstone of object-oriented design, and build upon your knowledge of vtables and dynamic dispatch.