Skip to content
C++
Language
since C++98
Advanced

Virtual Inheritance

Share a single base subobject across a diamond hierarchy with `virtual` bases: layout, construction order, casting costs, and pitfalls.

Virtual inheritancesince C++98

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

cpp
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 object

Note 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

cpp
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 subobject
  • virtual and 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.

cpp
#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:

  1. Virtual base subobjects, in depth-first, left-to-right order of the inheritance graph.
  2. Direct non-virtual bases, in declaration order.
  3. Non-static data members, in declaration order.
  4. 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

cpp
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

cpp
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 virtual retroactively 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; virtual must appear on Mammal : virtual Animal and Winged : 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_cast and reinterpret_cast-style tricks through a virtual base fail or are undefined. Use dynamic_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 /vmv and /vmg options 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.
cpp
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)

See Also