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.

Note
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.

Warning
"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.

Best Practice
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
Warning
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 virtual in 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 virtual in the inheritance lists of the classes that should share, as in class 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.