Multiple Inheritance and Virtual Inheritance
C++ multiple inheritance — diamond problem, virtual base classes, construction order, dominant overriding, and MI best practices.
Multiple Inheritancesince C++98A 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
// 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 subobjectAccess 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:
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 dThe 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
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
// 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"; // OKThe 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:
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 / CFull order for any derived object:
- All virtual bases, depth-first left-to-right through the inheritance graph.
- Non-virtual direct bases, left-to-right.
- Non-static data members in declaration order.
- The constructor body.
Destructors run in exact reverse.
Dominant override
When a virtual function appears in a virtual base, the most-derived override dominates:
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::fIf 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):
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:
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
// ✅ 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
- CRTP — stateless mixin composition without vtables
- Virtual Functions — vtable mechanics, override and final
- Policy-Based Design — template-driven behaviour composition
- Object Layout — vptr placement, EBO, padding
- Abstract Classes — pure-interface pattern details