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.
Diamond problem and virtual inheritance
Two bases that share a common ancestor without virtual produce two independent copies of that ancestor inside the derived object. Every access to the shared ancestor through the derived type is then ambiguous, and the two copies can drift into inconsistent state. Virtual inheritance replaces the two copies with a single shared subobject, located via a per-class vbptr at runtime.
// C++98 — non-virtual diamond: two separate Animal subobjects
struct Animal { int hp{100}; };
struct Warrior : Animal {}; // has Warrior::Animal
struct Mage : Animal {}; // has Mage::Animal
struct Hero : Warrior, Mage {};
Hero h;
// h.hp = 50; // ERROR: ambiguous — Warrior::Animal::hp or Mage::Animal::hp?
h.Warrior::hp = 50; // explicit qualification selects one copy
h.Mage::hp = 75; // the other copy is untouched — silent inconsistency
// Virtual inheritance — one shared Animal subobject
struct VWarrior : virtual Animal {};
struct VMage : virtual Animal {};
struct VHero : VWarrior, VMage {};
VHero vh;
vh.hp = 50; // unambiguous — there is exactly one Animal subobjectA vbptr is injected into every intermediate class that uses virtual inheritance. The pointer enables the compiler to locate the shared subobject regardless of which subobject pointer is in use. This indirection costs one pointer per intermediate base and prevents several pointer casts from being compile-time constants.
Virtual base class construction order
The most-derived class is always responsible for constructing every virtual base, regardless of which intermediate class originally declared the virtual relationship. Intermediate mem-initialisers for virtual bases are silently skipped when a more-derived class takes over the construction. Failing to initialise a virtual base that lacks a default constructor is a compile error, not a runtime surprise.
// C++11
struct Config {
int verbosity;
explicit Config(int v) : verbosity{v} {}
};
struct LoggingBase : virtual Config {
LoggingBase() : Config{1} {} // executed only when LoggingBase is most-derived
};
struct StorageBase : virtual Config {
StorageBase() : Config{2} {} // executed only when StorageBase is most-derived
};
struct Application : LoggingBase, StorageBase {
// Must explicitly initialise Config here — the intermediate initialisers above are skipped
Application() : Config{3}, LoggingBase{}, StorageBase{} {}
};
// Application a{}; => Config(3), then LoggingBase body, then StorageBase body, then Application bodyThe full construction sequence for any object is:
- All virtual bases, depth-first left-to-right through the inheritance DAG.
- Non-virtual direct bases, left-to-right per the base-specifier list.
- Non-static data members, in declaration order.
- The constructor body.
Destruction is the exact reverse. Move and copy constructors follow the same ordering.
Interface-only bases (pure virtual + no data)
Inheriting multiple pure-abstract interfaces is the safest and most idiomatic application of MI. An interface carries no data members, no non-virtual member functions, and a virtual destructor; it contributes only vtable entries to the derived class layout.
// C++20 — interfaces modelled as pure-abstract classes
struct IReader {
virtual std::vector<std::byte> read(std::size_t n) = 0;
virtual ~IReader() = default;
};
struct IWriter {
virtual void write(std::span<const std::byte> data) = 0; // std::span: C++20
virtual ~IWriter() = default;
};
struct ISeekable {
virtual void seek(std::ptrdiff_t offset) = 0;
virtual std::size_t tell() const = 0;
virtual ~ISeekable() = default;
};
// File implements all three interfaces — no ambiguity, each interface is empty
class File : public IReader, public IWriter, public ISeekable {
int fd_;
public:
explicit File(int fd) : fd_{fd} {}
std::vector<std::byte> read(std::size_t n) override;
void write(std::span<const std::byte>) override;
void seek(std::ptrdiff_t offset) override;
std::size_t tell() const override;
};
void copy_bytes(IReader& src, IWriter& dst, std::size_t n) {
dst.write(src.read(n)); // works with any IReader / IWriter pair
}Each interface base adds one vptr to the object (unless it shares a vtable with a previous base). A File* converted to IWriter* or ISeekable* will have a different raw address — never compare interface pointers as void*.
Mixin bases via multiple inheritance
Stateless CRTP mixins inject reusable behaviour at zero runtime cost by exploiting the empty-base optimisation (EBO). Each mixin base is templated on the derived type, so it can call back into the concrete class without a vtable.
// C++11 — CRTP mixin pattern
template<typename Derived>
struct Printable {
void print() const {
static_cast<const Derived*>(this)->to_stream(std::cout);
std::cout << '\n';
}
};
template<typename Derived>
struct Comparable {
friend bool operator< (const Derived& a, const Derived& b) { return a.cmp(b) < 0; }
friend bool operator<=(const Derived& a, const Derived& b) { return a.cmp(b) <= 0; }
friend bool operator> (const Derived& a, const Derived& b) { return a.cmp(b) > 0; }
friend bool operator>=(const Derived& a, const Derived& b) { return a.cmp(b) >= 0; }
};
// C++20: the same goal is achievable with operator<=> + std::rel_ops,
// but the CRTP form remains useful when a custom projection is needed.
class Version : public Printable<Version>,
public Comparable<Version> {
int major_, minor_, patch_;
public:
Version(int maj, int min, int pat) : major_{maj}, minor_{min}, patch_{pat} {}
void to_stream(std::ostream& os) const {
os << major_ << '.' << minor_ << '.' << patch_;
}
int cmp(const Version& o) const noexcept {
if (major_ != o.major_) return major_ - o.major_;
if (minor_ != o.minor_) return minor_ - o.minor_;
return patch_ - o.patch_;
}
};
Version v1{1, 2, 0}, v2{1, 3, 0};
v1.print(); // "1.2.0"
bool older = v1 < v2; // true — from Comparable mixin
static_assert(sizeof(Version) == 3 * sizeof(int)); // EBO: both mixin bases are emptyApplying [[no_unique_address]] (C++20) to mixin-as-member achieves the same layout savings when inheritance is not appropriate.
Name collision and explicit qualification
When two bases expose the same name, unqualified lookup is ambiguous at compile time regardless of whether the signatures differ. The fix is a qualified call (Base::member), a using-declaration that promotes one base's name into the derived scope, or a forwarding override that merges both.
// C++17
struct Sensor { std::string_view status() const { return "sensor-ok"; } };
struct Radio { std::string_view status() const { return "radio-ok"; } };
struct Device : Sensor, Radio {
// Option 1 — qualified calls where needed:
void report() const {
std::cout << Sensor::status() << ' ' << Radio::status() << '\n';
}
// Option 2 — bring one into unqualified scope:
using Sensor::status; // Device::status() now means Sensor::status()
// Option 3 — provide a unified override:
// std::string status() const { return std::string{Sensor::status()} + "/" + std::string{Radio::status()}; }
};
Device d;
d.Sensor::status(); // always unambiguous
d.Radio::status();
d.status(); // resolves to Sensor::status() because of the using-declarationA using-declaration that promotes Base1::name does not pull in overloads from Base2::name. To expose all overloads from both bases under the same unqualified name, add a using-declaration for each base separately.
Multiple inheritance vs composition
MI and composition both model "a thing that can do X and Y"; the choice affects coupling, testability, and object identity. Use MI for interfaces and stateless mixins; prefer composition when the component has observable state, needs independent lifetime management, or must be replaced at runtime.
// C++17 — composition alternative to MI for stateful components
class Logger {
public:
void log(std::string_view msg) { /* write to sink */ }
};
class Metrics {
public:
void record(std::string_view key, double value) { /* update counter */ }
};
// MI version — Logger and Metrics are bases
class ServiceMI : public Logger, public Metrics {
public:
void handle() { log("handling"); record("calls", 1.0); }
};
// Composition version — Logger and Metrics are members
class ServiceComp {
Logger logger_;
Metrics metrics_;
public:
void handle() { logger_.log("handling"); metrics_.record("calls", 1.0); }
};With MI, ServiceMI is-a Logger — any code that accepts a Logger& can receive a ServiceMI&. With composition, ServiceComp has-a Logger — the component is private and cannot be passed directly. Choose based on which Liskov substitution relationship is correct for the domain.
ABI implications of multiple inheritance
Multiple inheritance forces the compiler to adjust this pointers when converting between base types, which has observable ABI consequences. For non-first bases, a pointer-to-base holds an address that differs from the pointer-to-derived by a fixed (but compiler-specific) byte offset.
// C++11
struct A { int a{1}; };
struct B { int b{2}; };
struct C : A, B { int c{3}; };
C obj;
A* ap = &obj; // same address as &obj (first base)
B* bp = &obj; // address of obj + sizeof(A) + any padding — typically &obj + 4
C* cp = &obj;
static_assert(sizeof(C) == 3 * sizeof(int));
// Correct comparison: convert both through the most-derived type
bool same_object = (static_cast<C*>(ap) == static_cast<C*>(bp)); // true
// Wrong: reinterpret_cast or (void*) comparison may yield falseVirtual inheritance adds a further indirection: the offset from an intermediate base's address to the virtual base's address is not a compile-time constant — it lives in a virtual-base table (vbtable) indexed at runtime. This means:
- Casting a virtual-base pointer back to an intermediate type requires RTTI (
dynamic_cast). - Storing raw offsets for serialisation or IPC is fragile; prefer stable integer identifiers instead.
- Passing objects with virtual bases across shared-library boundaries is only safe when both sides were compiled with the same ABI (same compiler, same flags, same vtable layout).
// C++11 — dynamic_cast is the only safe downcast through virtual inheritance
struct VBase { virtual ~VBase() = default; }; // must be polymorphic
struct VDerived : virtual VBase {};
struct VLeaf : VDerived {};
VBase* vb = new VLeaf{};
VDerived* vd = dynamic_cast<VDerived*>(vb); // safe: O(1) with RTTI
// static_cast<VDerived*>(vb); // undefined behaviour — offset unknown
delete vb;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