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

Multiple Inheritance and Virtual Inheritance

C++ multiple inheritance — diamond problem, virtual base classes, construction order, dominant overriding, and MI best practices.

Multiple Inheritancesince C++98

A C++ class may derive from more than one base class simultaneously; each base contributes a distinct subobject to the derived object's layout, and name lookup searches all bases, making ambiguity resolution and shared-ancestor management explicit compiler concerns.

Overview

Multiple inheritance (MI) is present since C++98 and is neither inherently dangerous nor exotic. It underpins pure-interface composition, mixin libraries, and policy-based design. Two problems surface when MI is applied naively:

Name ambiguity — two bases expose the same member name; unqualified lookup fails at compile time.

Replicated base subobjects — when two bases share a common ancestor, the derived object contains two copies of that ancestor. This is the diamond problem. Virtual inheritance solves it by arranging a single shared subobject, but requires the most-derived class to construct the virtual base directly and adds a virtual-base-table pointer (vbptr) per intermediate class to locate the shared subobject at runtime.

The practical rule: inherit multiple pure-abstract interfaces freely; use virtual inheritance only when the diamond genuinely models the problem domain.

Syntax

cpp
// Non-virtual MI: each base gets its own subobject
class Derived : public Base1, public Base2 { /* ... */ };

// Virtual MI: mark the shared base virtual in every intermediate class
class Mid1 : virtual public Shared { /* ... */ };
class Mid2 : virtual public Shared { /* ... */ };
class Leaf  : public Mid1, public Mid2 { /* ... */ };  // one Shared subobject

Access specifiers apply independently per base. The left-to-right order of the base list determines non-virtual-base construction order and subobject layout order.

Examples

Pure-interface composition

The safest and most idiomatic use of MI — inherit only pure-abstract classes that carry no data:

cpp
struct ISerializable {
    virtual std::vector<std::byte> serialize() const = 0;
    virtual void deserialize(std::span<const std::byte>) = 0;  // std::span: C++20
    virtual ~ISerializable() = default;  // C++11
};

struct IDrawable {
    virtual void draw(Canvas&) const = 0;
    virtual ~IDrawable() = default;
};

class Widget : public ISerializable, public IDrawable {
public:
    std::vector<std::byte> serialize() const override { return {}; }
    void deserialize(std::span<const std::byte>) override {}
    void draw(Canvas&) const override {}
};

Widget w;
IDrawable*     d = &w;  // OK; pointer may be adjusted by the compiler
ISerializable* s = &w;  // OK; typically a different address than d

The compiler adjusts pointer values when converting to a non-first base. Comparing (void*)d == (void*)s is meaningless and incorrect — always compare through a common type.

Name ambiguity and resolution

cpp
struct Logger  { void log(std::string_view msg); };  // std::string_view: C++17
struct Auditor { void log(std::string_view msg); };

struct Service : Logger, Auditor {};

Service svc;
// svc.log("event");       // ERROR: ambiguous — Logger::log or Auditor::log?
svc.Logger::log("event");  // qualified call: explicit
svc.Auditor::log("event");

// A using-declaration selects one spelling unambiguously:
struct Service2 : Logger, Auditor {
    using Logger::log;  // unqualified log() now resolves to Logger::log
};

Diamond problem and virtual inheritance

cpp
// Without virtual: two Animal subobjects, every access is ambiguous
struct Animal  { std::string name; };
struct Canine  : Animal {};
struct Machine : Animal {};
struct Cyborg  : Canine, Machine {};

Cyborg c;
// c.name = "Rex";        // ERROR: Canine::Animal::name vs Machine::Animal::name
c.Canine::name  = "Rex";  // disambiguate explicitly

// With virtual: one shared Animal subobject
struct CanineV  : virtual Animal {};
struct MachineV : virtual Animal {};
struct CyborgV  : CanineV, MachineV {};

CyborgV cv;
cv.name = "Rex";  // OK

The layout of CyborgV is implementation-defined. A common arrangement places the CanineV and MachineV subobjects first (each holding a vbptr), followed by the single Animal subobject at the end. Accessing cv.name through a CanineV* requires a runtime offset lookup via the vbptr — the cost of the shared-base guarantee.

Construction order with virtual bases

Virtual bases are always constructed by the most-derived class. Intermediate constructors' mem-initialisers for virtual bases are silently skipped when a more-derived class is being constructed:

cpp
struct V {
    int x;
    explicit V(int n) : x{n} { std::println("V({})", n); }  // std::println: C++23
};

struct A : virtual V {
    A() : V{1} { std::println("A"); }  // V{1} only executes when A is most-derived
};

struct B : virtual V {
    B() : V{2} { std::println("B"); }  // V{2} only executes when B is most-derived
};

struct C : A, B {
    C() : V{99}, A{}, B{} { std::println("C"); }
};
// Output: V(99) / A / B / C

Full order for any derived object:

  1. All virtual bases, depth-first left-to-right through the inheritance graph.
  2. Non-virtual direct bases, left-to-right.
  3. Non-static data members in declaration order.
  4. The constructor body.

Destructors run in exact reverse.

Dominant override

When a virtual function appears in a virtual base, the most-derived override dominates:

cpp
struct Base { virtual void f() { std::println("Base"); } };
struct Left  : virtual Base { void f() override { std::println("Left"); } };
struct Right : virtual Base {};  // no override

struct Diamond : Left, Right {};

Diamond d;
d.f();  // "Left" — Left::f dominates Base::f, no ambiguity

Base* bp = &d;
bp->f();  // "Left" — dynamic dispatch still resolves to Left::f

If both Left and Right override f, calling d.f() is a compile error; Diamond must provide its own override. The override in the most-derived class then dominates both.

CRTP mixin composition

Stateless CRTP mixins are the most practical production use of MI. They add behaviour without virtual dispatch and benefit from the empty-base optimisation (EBO, C++98 for single empty base; [[no_unique_address]] from C++20 handles member cases):

cpp
template<typename Derived>
struct EqualityComparable {
    friend bool operator==(const Derived& a, const Derived& b) noexcept {
        return a.equals(b);
    }
    friend bool operator!=(const Derived& a, const Derived& b) noexcept {
        return !a.equals(b);
    }
};

template<typename Derived>
struct Hashable {
    std::size_t hash() const noexcept {
        return static_cast<const Derived*>(this)->hash_impl();
    }
};

class UserId : public EqualityComparable<UserId>,
               public Hashable<UserId> {
    std::uint64_t id_;
public:
    explicit UserId(std::uint64_t id) : id_{id} {}
    bool        equals(const UserId& o) const noexcept { return id_ == o.id_; }
    std::size_t hash_impl()            const noexcept {
        return std::hash<std::uint64_t>{}(id_);
    }
};

UserId a{42}, b{42};
bool eq = (a == b);   // true — zero overhead, no vtable
auto h  = a.hash();

Both mixin bases are empty; sizeof(UserId) == sizeof(std::uint64_t).

Policy-based design

Template template parameters with MI give compile-time policy selection — a pattern pioneered in early C++ design libraries:

cpp
template<typename StoragePolicy, typename ThreadingPolicy>
class Cache : public StoragePolicy, public ThreadingPolicy {
public:
    void put(std::string_view key, std::string value) {
        auto lock = ThreadingPolicy::acquire();
        StoragePolicy::store(key, std::move(value));
    }
    std::optional<std::string> get(std::string_view key) {  // std::optional: C++17
        auto lock = ThreadingPolicy::acquire();
        return StoragePolicy::fetch(key);
    }
};

using ThreadSafeCache = Cache<MapStorage, MutexPolicy>;
using LocalCache      = Cache<MapStorage, NoLockPolicy>;

Name lookup through the base list replaces explicit forwarding calls; swapping a policy requires changing one type alias.

Best Practices

cpp
// ✅ Inherit multiple pure-abstract interfaces freely — no data, no ambiguity
class HttpHandler : public IRequestHandler,
                    public IAuthenticatable,
                    public ILoggable {};

// ✅ CRTP/stateless mixins for zero-overhead behavioural extension
class Point3D : public EqualityComparable<Point3D>,
                public Hashable<Point3D> {};

// ✅ When using virtual inheritance, always list the virtual base first
//    in the most-derived mem-init list so the intent is explicit
struct Leaf : Mid1, Mid2 {
    explicit Leaf(int x) : VirtualBase{x}, Mid1{}, Mid2{} {}
};

// ⚠️  Virtual bases add a vbptr per intermediate type and make copy/move
//    constructors non-trivial — profile before using on hot paths

// ❌ Inheriting concrete classes with state is almost always a design error
class Wrong : public std::vector<int>, public std::string {};

Common Pitfalls

Forgetting the virtual base in the most-derived mem-init. If Leaf omits the virtual base from its initialiser list and the virtual base has no default constructor, compilation fails — a useful diagnostic. If it does have a default constructor, the intermediate initialisers are silently ignored and the base is default-constructed; this is a correctness bug with no warning by default.

Pointer identity assumptions. After Derived* p = new Derived;, the raw addresses (void*)(Base1*)p and (void*)(Base2*)p are often different. Comparing them as void* is meaningless. Comparing them after converting both to Derived* is correct.

Overloads across two bases. A single using Base1::name; declaration does not import overloads from Base2::name. To expose all overloads from both bases under the same unqualified name, provide using declarations for both.

Non-virtual destructor in a virtual base. If the virtual base lacks a virtual destructor and objects are deleted through a pointer to it, behaviour is undefined. Add virtual ~Base() = default; (C++11) to any class used as a virtual base that may be deleted polymorphically.

Slicing through a non-first base. Assigning a Derived object to a Base2 value (not pointer) slices to the Base2 subobject. With MI the pointer adjustment makes the subobject's source address non-obvious, so the slice is harder to spot in review.

See Also