Resolving Multiple Inheritance Ambiguity
Avoid diamond inheritance problems with virtual base classes.
What Is a Virtual Base Class?
A virtual base class is a base that appears only once in the finished object, no matter how many inheritance paths lead to it. You ask for one by writing virtual in an inheritance list, and the compiler responds by giving every path a way to reach a single shared subobject instead of handing each path a private copy.
The reason to want that is best seen by first watching what happens without it. This lesson counts subobjects: how many exist by default, what goes wrong when there is more than one, what the virtual keyword changes about layout and construction, and what the sharing costs.
Virtual base classes and multiple inheritance are advanced material. Most C++ codebases never need either. Read this lesson to recognise the pattern when you meet it in someone else's hierarchy, not as a technique to reach for in your own designs.
Counting the Base Subobjects
Give two classes a common base, then derive a third class from both of them. Every constructor announces itself so the object's construction can be read off the output:
#include <iostream>
class Hull
{
private:
int m_lengthFeet{};
public:
Hull(int lengthFeet)
: m_lengthFeet{ lengthFeet }
{
std::cout << "Hull " << lengthFeet << " ft" << '\n';
}
int lengthFeet() const { return m_lengthFeet; }
};
class Sailboat : public Hull
{
public:
Sailboat(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Sailboat " << sailArea << " sq ft" << '\n';
}
};
class Motorboat : public Hull
{
public:
Motorboat(int horsepower, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Motorboat " << horsepower << " hp" << '\n';
}
};
class MotorSailer : public Sailboat, public Motorboat
{
public:
MotorSailer(int sailArea, int horsepower, int lengthFeet)
: Sailboat{ sailArea, lengthFeet }, Motorboat{ horsepower, lengthFeet }
{
}
};
int main()
{
MotorSailer craft{ 620, 180, 42 };
return 0;
}
Hull 42 ft
Sailboat 620 sq ft
Hull 42 ft
Motorboat 180 hp
Two Hull lines. Drawn on paper the hierarchy looks like a diamond with Hull at the top, but the object in memory is a Y with two separate trunks: one Hull living inside the Sailboat part, another living inside the Motorboat part. A MotorSailer is 42 feet long twice over, in two independent integers that nothing keeps in agreement.
This layout is called the diamond problem, and the name refers to the picture rather than to the trouble. Sometimes two copies are exactly right. If the shared base held a serial number rather than a length, one per component would be correct.
The Ambiguity That Follows
The trouble starts when code asks the shared base a question. Two subobjects could answer, the compiler has no basis for preferring either, and it refuses to guess, so the program below does not compile:
#include <iostream>
class Hull
{
private:
int m_lengthFeet{};
public:
Hull(int lengthFeet)
: m_lengthFeet{ lengthFeet }
{
}
int lengthFeet() const { return m_lengthFeet; }
};
class Sailboat : public Hull
{
public:
Sailboat(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Sailboat " << sailArea << " sq ft" << '\n';
}
};
class Motorboat : public Hull
{
public:
Motorboat(int horsepower, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Motorboat " << horsepower << " hp" << '\n';
}
};
class MotorSailer : public Sailboat, public Motorboat
{
public:
MotorSailer(int sailArea, int horsepower, int lengthFeet)
: Sailboat{ sailArea, lengthFeet }, Motorboat{ horsepower, lengthFeet }
{
}
};
int main()
{
MotorSailer craft{ 620, 180, 42 };
std::cout << craft.lengthFeet() << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:49:24: error: request for member 'lengthFeet' is ambiguous
49 | std::cout << craft.lengthFeet() << '\n';
| ^~~~~~~~~~
GCC goes on to list the two candidates it found, and both of them read int Hull::lengthFeet() const. Identical signature, identical class, different subobject. You can break the tie by naming a path, writing craft.Sailboat::lengthFeet() or craft.Motorboat::lengthFeet(), but that is a disguise rather than a fix. Every caller now has to know which trunk to walk down, and the two trunks can disagree.
One Shared Subobject with virtual
Put virtual in the inheritance list of each class that should share the base, and the duplication goes away:
class Hull
{
};
class Sailboat : virtual public Hull
{
};
class Motorboat : virtual public Hull
{
};
class MotorSailer : public Sailboat, public Motorboat
{
};
Two details about that syntax are easy to get backwards.
The virtual goes on the classes that will be sharing, not on the class that joins them. MotorSailer inherits normally; Sailboat and Motorboat are the ones declaring that their Hull is negotiable. A class only shares a base with siblings that also asked for sharing, so a third class deriving non-virtually from Hull would still bring its own copy along.
The keyword also has nothing to do with virtual functions beyond the spelling. virtual in an inheritance list changes object layout; virtual on a member function changes dispatch. They reuse one keyword for two unrelated jobs.
Who Constructs the Shared Base
Sharing raises a question that ordinary inheritance never has to answer. If Sailboat and Motorboat both list Hull{ lengthFeet } in their member initializer lists, and there is now only one Hull to build, which of them builds it?
Neither. The most derived class does, and it must say so explicitly even though Hull is not its direct base. Leave the initializer out and the code below does not compile, because the compiler goes looking for a default constructor that Hull does not have:
#include <iostream>
class Hull
{
public:
Hull(int lengthFeet)
{
std::cout << "Hull " << lengthFeet << " ft" << '\n';
}
};
class Sailboat : virtual public Hull
{
public:
Sailboat(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Sailboat " << sailArea << " sq ft" << '\n';
}
};
class Motorboat : virtual public Hull
{
public:
Motorboat(int horsepower, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Motorboat " << horsepower << " hp" << '\n';
}
};
class MotorSailer : public Sailboat, public Motorboat
{
public:
MotorSailer(int sailArea, int horsepower, int lengthFeet)
: Sailboat{ sailArea, lengthFeet }
, Motorboat{ horsepower, lengthFeet }
{
}
};
int main()
{
MotorSailer craft{ 620, 180, 42 };
return 0;
}
s.cpp: In constructor 'MotorSailer::MotorSailer(int, int, int)':
s.cpp:37:45: error: no matching function for call to 'Hull::Hull()'
37 | , Motorboat{ horsepower, lengthFeet }
| ^
Naming Hull directly in MotorSailer's initializer list fixes it. This is one of the few places C++ permits a class to initialise a base that is not its immediate parent, and it exists precisely because someone has to own the shared subobject:
#include <iostream>
class Hull
{
private:
int m_lengthFeet{};
public:
Hull(int lengthFeet)
: m_lengthFeet{ lengthFeet }
{
std::cout << "Hull " << lengthFeet << " ft" << '\n';
}
int lengthFeet() const { return m_lengthFeet; }
};
class Sailboat : virtual public Hull
{
public:
Sailboat(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Sailboat " << sailArea << " sq ft" << '\n';
}
};
class Motorboat : virtual public Hull
{
public:
Motorboat(int horsepower, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Motorboat " << horsepower << " hp" << '\n';
}
};
class MotorSailer : public Sailboat, public Motorboat
{
public:
MotorSailer(int sailArea, int horsepower, int lengthFeet)
: Hull{ lengthFeet }
, Sailboat{ sailArea, lengthFeet }
, Motorboat{ horsepower, lengthFeet }
{
}
};
int main()
{
MotorSailer craft{ 620, 180, 42 };
std::cout << "length " << craft.lengthFeet() << '\n';
return 0;
}
Hull 42 ft
Sailboat 620 sq ft
Motorboat 180 hp
length 42
One Hull line, and craft.lengthFeet() now compiles because there is only one function it could mean. The Hull{ lengthFeet } initializers still sitting in Sailboat and Motorboat were skipped: when the most derived class takes charge of the shared base, the intermediate classes' requests for it are ignored.
"Ignored" means the arguments are never evaluated either. If
Sailboat passed a computed value to Hull, that computation does not run while a MotorSailer is being built, and the value MotorSailer supplies wins. Never rely on an intermediate class's virtual base initializer to enforce an invariant.
Responsibility follows the most derived class even when nothing about the hierarchy looks like a diamond. A single-inheritance chain over a virtual base behaves the same way:
#include <iostream>
class Hull
{
public:
Hull(int lengthFeet)
{
std::cout << "Hull " << lengthFeet << " ft" << '\n';
}
};
class Sailboat : virtual public Hull
{
public:
Sailboat(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Sailboat " << sailArea << " sq ft" << '\n';
}
};
class Daysailer : public Sailboat
{
public:
Daysailer(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
, Sailboat{ sailArea, lengthFeet }
{
std::cout << "Daysailer ready" << '\n';
}
};
int main()
{
Daysailer dinghy{ 145, 19 };
return 0;
}
Hull 19 ft
Sailboat 145 sq ft
Daysailer ready
Daysailer has exactly one parent and still has to initialise Hull itself. Making a base virtual pushes that obligation all the way down to whichever class is most derived, forever.
The Intermediate Initializer Is Not Dead Code
The Hull{ lengthFeet } line inside Sailboat is skipped when a MotorSailer is built, but it is not removable. Construct a Sailboat on its own and it becomes the most derived class, which makes it responsible for its own Hull:
#include <iostream>
class Hull
{
private:
int m_lengthFeet{};
public:
Hull(int lengthFeet)
: m_lengthFeet{ lengthFeet }
{
std::cout << "Hull " << lengthFeet << " ft" << '\n';
}
int lengthFeet() const { return m_lengthFeet; }
};
class Sailboat : virtual public Hull
{
public:
Sailboat(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Sailboat " << sailArea << " sq ft" << '\n';
}
};
class Motorboat : virtual public Hull
{
public:
Motorboat(int horsepower, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Motorboat " << horsepower << " hp" << '\n';
}
};
class MotorSailer : public Sailboat, public Motorboat
{
public:
MotorSailer(int sailArea, int horsepower, int lengthFeet)
: Hull{ lengthFeet }
, Sailboat{ sailArea, lengthFeet }
, Motorboat{ horsepower, lengthFeet }
{
}
};
int main()
{
Sailboat sloop{ 310, 27 };
std::cout << "---" << '\n';
MotorSailer craft{ 620, 180, 42 };
return 0;
}
Hull 27 ft
Sailboat 310 sq ft
---
Hull 42 ft
Sailboat 620 sq ft
Motorboat 180 hp
The same source line ran for sloop and was passed over for craft. Which behaviour you get is decided by the object being built, not by the constructor being written, so an intermediate class has to supply a usable initializer for every case where it might turn out to be the most derived class.
Virtual Bases Are Built First
When a class has both virtual and non-virtual bases, the virtual ones are constructed before any of them, ahead of declaration order. This guarantees the shared subobject exists before any path that might reach it starts running.
MotorSailer below lists Registration, a plain non-virtual base, first among its bases:
#include <iostream>
class Hull
{
public:
Hull(int lengthFeet)
{
std::cout << "Hull " << lengthFeet << " ft" << '\n';
}
};
class Registration
{
public:
Registration(int hullNumber)
{
std::cout << "Registration " << hullNumber << '\n';
}
};
class Sailboat : virtual public Hull
{
public:
Sailboat(int sailArea, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Sailboat " << sailArea << " sq ft" << '\n';
}
};
class Motorboat : virtual public Hull
{
public:
Motorboat(int horsepower, int lengthFeet)
: Hull{ lengthFeet }
{
std::cout << "Motorboat " << horsepower << " hp" << '\n';
}
};
class MotorSailer : public Registration, public Sailboat, public Motorboat
{
public:
MotorSailer(int sailArea, int horsepower, int lengthFeet, int hullNumber)
: Hull{ lengthFeet }
, Registration{ hullNumber }
, Sailboat{ sailArea, lengthFeet }
, Motorboat{ horsepower, lengthFeet }
{
}
};
int main()
{
MotorSailer craft{ 620, 180, 42, 7391 };
return 0;
}
Hull 42 ft
Registration 7391
Sailboat 620 sq ft
Motorboat 180 hp
Hull runs first despite Registration being the first base listed. After the virtual bases, the remaining bases run in declaration order as usual.
Write the member initializer list in the order things are actually constructed: virtual bases, then non-virtual bases in declaration order, then data members. GCC's
-Wreorder warns when the written order and the real order disagree, and the warning is worth heeding because a reader who trusts the written order will misread the code.
What the Sharing Costs
A single shared subobject cannot sit at a fixed offset from every path that reaches it, because each path has its own layout. Implementations solve this by storing the offset to the shared base and looking it up when needed, which means an object with a virtual base carries a hidden pointer it would not otherwise need. On the platform compiler that pointer is the same virtual table pointer used for virtual functions, so a class gains one even if it declares no virtual functions at all:
#include <iostream>
class Keel
{
public:
int m_ballastPounds{};
};
class PlainFin : public Keel
{
};
class SharedFin : virtual public Keel
{
};
int main()
{
std::cout << "plain " << sizeof(PlainFin) << '\n';
std::cout << "shared " << sizeof(SharedFin) << '\n';
return 0;
}
plain 4
shared 16
Neither class declares a single virtual function. PlainFin is just its inherited int. SharedFin adds an eight-byte pointer, and alignment rounds the total up to sixteen. The exact numbers are implementation-defined and will differ on other compilers and architectures; what generalises is that the class grew by at least a pointer, and that reaching members of a virtual base costs an indirection rather than a constant offset.
When to Reach for It
Virtual inheritance is the right answer to one specific question: does the shared base represent one thing or two? Ask it about the data, not about the diagram.
| The shared base holds | Two copies are | Use |
|---|---|---|
| Identity or state describing the whole object, such as a length or an owner | Wrong, and can silently disagree | Virtual inheritance |
| State belonging to each component separately, such as a per-part serial number | Correct | Plain inheritance |
| Nothing at all, a pure interface with no data members | Harmless either way | Plain inheritance, and prefer interfaces here |
Virtual inheritance repairs a hierarchy; it does not justify one. Before adding the keyword, check whether the class needs to inherit from both parents at all. Holding one of them as a member instead removes the shared base, the ambiguity, the construction rule, and the size cost in a single edit.
Looking Forward
The pointer that makes virtual bases work is the same machinery this chapter's virtual functions rely on, which is why the size cost lands in classes with no virtual functions of their own. Once you have seen how a vtable pointer supports run-time dispatch, come back to the sizeof output above: the compiler is using one mechanism to answer two different questions, "which override do I call" and "where is my shared base".
Key Terminology
- Virtual base class: a base declared with
virtualin an inheritance list, of which exactly one subobject exists in the finished object regardless of how many paths lead to it. - Diamond problem: the shape produced when a class inherits from two classes sharing a common base, which by default gives the object two copies of that base.
- Subobject: the region of a derived object corresponding to one of its base classes. Duplicate base subobjects are what makes member access ambiguous.
- Most derived class: the actual type of the object being constructed, which is the class responsible for initialising every virtual base in the hierarchy.
- Member initializer list: the
: Base{ ... }, m_member{ ... }clause of a constructor, and the only place a virtual base can be given arguments.
Summary
| Question | Plain inheritance | Virtual inheritance |
|---|---|---|
| How many base subobjects per path? | One each | One shared by all |
| Who initialises the base? | Each immediate child | The most derived class, always |
| What happens to intermediate initializers? | They run | Skipped when a more derived class is being built |
| When is the base constructed? | In base declaration order | Before every non-virtual base |
| What does an object cost? | Just the inherited members | Plus a hidden pointer per object |
| Is an inherited member ambiguous? | Yes, once two copies exist | No, there is only one |
- Write
virtualin the inheritance lists of the classes that should share, as inclass Sailboat : virtual public Hull. The class that joins them inherits normally. - The most derived class must name the virtual base in its member initializer list, even when that base is not its direct parent, and this applies to single-inheritance chains too.
- Intermediate classes still need their own virtual base initializer for the case where they are constructed standalone, but it is skipped, arguments and all, when something more derived is being built.
- Virtual bases are constructed before non-virtual bases, ahead of declaration order.
- A class with a virtual base gains a hidden pointer even with no virtual functions, so the sharing is not free.
- Reach for virtual inheritance when two copies of the shared state would be a contradiction. When they would simply be redundant, composition is the cheaper repair.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Resolving Multiple Inheritance Ambiguity - Quiz
Test your understanding of the lesson.
Practice Exercises
Virtual Base Classes and Diamond Problem
Solve the diamond problem in multiple inheritance using virtual base classes. Understand how virtual inheritance ensures only one instance of the base class exists.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!