Virtual Inheritance
Share a single base subobject across a diamond hierarchy with `virtual` bases: layout, construction order, casting costs, and pitfalls.
Virtual inheritancesince C++98A derivation written class D : virtual public B that makes all such derivations of B within a most-derived object share a single B base subobject, whose construction is performed by the most-derived class.
Overview
With multiple inheritance, a class can reach the same base through more than one path. Without virtual, each path contributes its own subobject, so the "diamond" yields two copies of the common base and ambiguous member access. Declaring the intermediate derivations virtual collapses these into one shared subobject.
struct Animal { int age = 0; };
struct Mammal : Animal {};
struct Winged : Animal {};
struct Bat : Mammal, Winged {};
Bat b;
// b.age; // error: ambiguous (two Animal subobjects)
b.Mammal::age = 1; // legal, but Winged::age is a distinct objectNote that virtual here is unrelated to virtual functions, though both involve runtime indirection. The keyword must be applied at the intermediate classes (Mammal, Winged), not at the join point (Bat). This requires foresight: the base class author of the middle layer must anticipate the diamond.
Syntax
struct Animal { /* ... */ };
struct Mammal : virtual Animal {}; // private by default for class, public for struct
struct Winged : virtual public Animal {}; // `virtual` and access specifier may appear in either order
struct Bat : Mammal, Winged {}; // one Animal subobjectvirtualand the access specifier can be written in either order.- A class can have the same base both virtually and non-virtually via different paths; the virtual paths share one subobject and each non-virtual path gets its own.
- Access to a shared base is the most permissive among the paths (name lookup rules about dominance apply).
Examples
Construction order and the most-derived rule
The most-derived class initializes every virtual base directly. Mem-initializers for the virtual base in intermediate classes are ignored when those classes are not the most-derived type.
#include <iostream>
#include <string>
struct Device {
std::string id;
explicit Device(std::string i) : id(std::move(i)) { std::cout << "Device(" << id << ")\n"; }
virtual ~Device() = default; // virtual dtor: see Best Practices
};
struct Scanner : virtual Device {
Scanner() : Device("scanner-ignored") { std::cout << "Scanner\n"; }
};
struct Printer : virtual Device {
Printer() : Device("printer-ignored") { std::cout << "Printer\n"; }
};
struct Copier : Scanner, Printer {
Copier() : Device("copier-0"), Scanner(), Printer() { std::cout << "Copier\n"; }
};
int main() {
Copier c; // Device(copier-0), Scanner, Printer, Copier
std::cout << c.id << '\n'; // copier-0, unambiguous
}Order of initialization for a most-derived object:
- Virtual base subobjects, in depth-first, left-to-right order of the inheritance graph.
- Direct non-virtual bases, in declaration order.
- Non-static data members, in declaration order.
- The constructor body.
Destruction runs in exact reverse. Because every most-derived class must be able to construct the virtual base, a virtual base with no default constructor forces every further-derived class to name it explicitly.
Casting through a virtual base
struct Shape { virtual ~Shape() = default; };
struct Rect : virtual Shape {};
struct Rounded : virtual Shape {};
struct RoundedRect : Rect, Rounded {};
void use(Shape& s) {
// static_cast<RoundedRect&>(s); // ill-formed: cannot downcast from a virtual base
if (auto* rr = dynamic_cast<RoundedRect*>(&s)) { // OK: needs polymorphic Shape (RTTI)
(void)rr;
}
}static_cast downcasting from a virtual base is ill-formed because the offset of the base within the derived object is not fixed; it is only discoverable at run time. dynamic_cast works since it consults the dynamic type (requires a polymorphic base). Upcasts are always implicit and safe, but not free (see below).
Interface-style usage
struct IReadable { virtual int read() = 0; virtual ~IReadable() = default; };
struct IWritable { virtual void write(int) = 0; virtual ~IWritable() = default; };
struct IResource : virtual IReadable, virtual IWritable {};Pure-interface bases with no data are the most defensible use of virtual inheritance: no state to initialize, no constructor-argument burden.
Best Practices
- Prefer stateless virtual bases. Interfaces with only pure virtual functions (and a virtual destructor) avoid most of the construction pitfalls.
- Give the virtual base a default constructor so intermediate and further-derived classes need not know about it.
- Declare a virtual destructor in a polymorphic virtual base so deletion via
Base*is well-defined. - Name the virtual base explicitly in the most-derived constructor when it requires arguments; do not rely on intermediate classes.
- Consider alternatives first: composition, or a design where the shared concept is a member rather than a base. A diamond often indicates a missing discriminator in the design.
- Do not add
virtualretroactively across a public ABI: it changes object layout and constructor semantics, breaking binary compatibility.
Common Pitfalls
- Ignored mem-initializers. An intermediate class's
: Device("x")runs only when that class is itself the complete object. Reading code that suggests otherwise is a frequent source of bugs. - Forgetting the diamond is closed by the intermediates. Marking only
Bat's derivations virtual does nothing;virtualmust appear onMammal : virtual AnimalandWinged : virtual Animal. - Runtime cost. Access to a virtual base member goes through a pointer or offset stored in the object (vbptr, or a vtable entry, implementation-defined). Objects grow, the hot path gains an indirection, and the class is never trivially copyable or standard-layout. It also is not an aggregate in C++11/14 and, as of C++17, aggregates cannot have virtual base classes either (C++17 allowed public non-virtual bases only).
- Copy/move duplication. Implicit copy and move assignment of a diamond can assign the virtual base subobject more than once (once per path), which is wasteful, and for move can leave it moved-from before a later path reads it. The standard specifies that the implicit move of a virtual base is performed only once in C++11 and later for implicitly defined operators, but user-written assignment operators in intermediates will still run the base's assignment for each path unless you guard against it. Prefer deleting or centralizing copy in the most-derived class.
- Downcasts.
static_castandreinterpret_cast-style tricks through a virtual base fail or are undefined. Usedynamic_cast, which requires RTTI and costs time. - Pointer-to-member and ABI. Pointers to members of virtual bases are larger and slower on common ABIs, and layout differs between Itanium and MSVC. MSVC even exposes
/vmvand/vmgoptions for representation models of member pointers. - Calling virtual functions during construction. In a diamond, a virtual base's constructor runs before the derived parts exist; calls dispatch to the base version only.
- Ambiguous overriders. If two paths each override the same virtual function from the shared base and no final overrider exists in the most-derived class, the program is ill-formed. Provide the final overrider in the join class.
struct B { virtual void f() = 0; virtual ~B() = default; };
struct L : virtual B { void f() override {} };
struct R : virtual B { void f() override {} };
struct D : L, R { void f() override {} }; // required; otherwise ill-formed (no unique final overrider)