C++ / INHERITANCE AND POLYMORPHISM
Multiple inheritance and the diamond problem
Use multiple inheritance in C++, spot why a shared base gets duplicated, and fix it with virtual inheritance while knowing who initializes the shared base.
What you will learn
- Disambiguate an inherited member with Base::name when two bases declare that name
- Spot a non-virtual diamond: two copies of the ancestor, two sets of its data members
- Mark a base virtual on every branch to get one shared subobject and a clean upcast
- Initialize a virtual base from the most-derived class, never from the middle classes
Understanding Multiple inheritance and the diamond problem
A class can list several bases: `struct Copier : Scanner, Printer {}`. The resulting object contains one complete subobject per base, laid out side by side in declaration order, so every base's data members and member functions exist independently inside the derived object. That layout is the source of the friction: if two bases declare a member with the same name, an unqualified use of that name in the derived class finds two different declarations in two different subobjects, and the compiler rejects it as ambiguous before it ever looks at parameter types. Naming the path — `c.Scanner::id`, `g.Serial::send(66)` — picks the subobject you meant.
The diamond appears when two of those bases share an ancestor. `Scanner : Device` and `Printer : Device` each carry a full Device, so `Copier` contains two Device subobjects with two independent `id` fields: writing through one branch leaves the other unchanged, `sizeof(Copier)` is twice what you expected, and even `Device* p = &c;` is ambiguous because there are two possible targets. The mental model to keep is that C++ flattens the inheritance graph into a tree of subobjects, one per path from the derived class up to the ancestor, so duplication is the default rather than an error.
Writing `virtual` in the base specifier — `struct Scanner : virtual Device` — asks for the opposite: all paths through a complete object share one Device subobject, so `c.id` is unambiguous and there is a single upcast target. Because that subobject belongs to the whole object rather than to one branch, the most-derived class initializes it: `Copier() : Device(9)` runs, and the `Device(1)` and `Device(2)` mem-initializers inside Scanner and Printer are ignored. It also means the offset from a Scanner subobject to its Device is no longer fixed at compile time, since it depends on which complete object that Scanner is embedded in, so access goes through a stored offset and `static_cast` from the virtual base back down is ill-formed. Mark the base virtual on every branch that inherits it, and prefer virtual bases that carry no data.
Virtual inheritance is a property of the base specifier, not of the base class, which is why it only works when every branch agrees.
<iostream>
struct Device {
int id;
explicit Device(int i) : id(i) {}
};
struct Scanner : Device {
Scanner() : Device(1) {}
};
struct Printer : Device {
Printer() : Device(2) {}
};
struct Copier : Scanner, Printer {};
int main() {
Copier c;
// plain c.id does not compile: which Device's id?
std::cout << "Scanner::id = " << c.Scanner::id << '\n';
std::cout << "Printer::id = " << c.Printer::id << '\n';
c.Scanner::id = 7;
std::cout << "after write, Printer::id = " << c.Printer::id << '\n';
Device* viaScanner = static_cast<Scanner*>(&c);
Device* viaPrinter = static_cast<Printer*>(&c);
std::cout << std::boolalpha
<< "same Device subobject? " << (viaScanner == viaPrinter) << '\n';
std::cout << "sizeof(Device)=" << sizeof(Device)
<< " sizeof(Copier)=" << sizeof(Copier) << '\n';
}
A derived object holds one subobject per inheritance path, so a shared ancestor is duplicated unless every path to it is declared virtual.
Worked examples
Sharing one base with virtual inheritance
Adding virtual to both branches collapses the two Device subobjects into one and moves responsibility for initializing it to Copier.
<iostream>
struct Device {
int id;
explicit Device(int i) : id(i) { std::cout << "Device(" << i << ")\n"; }
};
struct Scanner : virtual Device {
Scanner() : Device(1) { std::cout << "Scanner\n"; }
};
struct Printer : virtual Device {
Printer() : Device(2) { std::cout << "Printer\n"; }
};
struct Copier : Scanner, Printer {
Copier() : Device(9) { std::cout << "Copier\n"; }
};
int main() {
Copier c;
std::cout << "id = " << c.id << '\n';
Device* a = static_cast<Scanner*>(&c);
Device* b = static_cast<Printer*>(&c);
std::cout << std::boolalpha << "shared? " << (a == b) << '\n';
}
Example explained
Line 1`virtual Device` on both branches means the complete Copier object holds exactly one Device subobject, so `c.id` needs no qualification.
Line 2`Copier() : Device(9)` is what initializes it; the `Device(1)` and `Device(2)` mem-initializers are skipped because Scanner and Printer are not the most-derived class here.
Line 3The virtual base is constructed before any non-virtual base, which is why `Device(9)` prints before `Scanner`.
Line 4Both conversions produce the same address, so `a == b` is true where the non-virtual version printed false.
Same name in two unrelated bases
Ambiguity does not need a diamond: two independent bases declaring send collide, and using-declarations merge them into one overload set.
<iostream>
struct Serial {
void send(int byte) const { std::cout << "serial " << byte << '\n'; }
};
struct Network {
void send(const char* msg) const { std::cout << "net " << msg << '\n'; }
};
struct Gateway : Serial, Network {
using Serial::send;
using Network::send;
};
int main() {
Gateway g;
g.send(65);
g.send("hello");
g.Serial::send(66);
}
Example explained
Line 1Without the two using-declarations, `g.send(65)` is ambiguous: lookup finds `send` in two unrelated base classes and stops there, never comparing the `int` and `const char*` parameters.
Line 2`using Serial::send;` and `using Network::send;` bring both declarations into Gateway's own scope, where they form a single overload set.
Line 3Ordinary overload resolution then sends `65` to the `int` version and `"hello"` to the `const char*` version.
Line 4`g.Serial::send(66)` works with or without the using-declarations, because qualifying the call names the subobject instead of relying on lookup.
One final overrider through a virtual base
With a shared virtual base there is a single Base subobject, so an override in one branch is the one that runs for calls made through the other.
<iostream>
struct Base {
virtual void ping() const { std::cout << "Base::ping\n"; }
virtual ~Base() = default;
};
struct Left : virtual Base {
void ping() const override { std::cout << "Left::ping\n"; }
};
struct Right : virtual Base {};
struct Both : Left, Right {};
int main() {
Both b;
b.ping();
static_cast<Right&>(b).ping();
Base& base = b;
base.ping();
}
Example explained
Line 1`b.ping()` is unambiguous because Left::ping hides the Base::ping that Right's branch would otherwise contribute: both branches reach the same shared Base, and the declaration in the more derived class wins.
Line 2The call through `Right&` also lands in Left::ping, since Right does not own a separate Base subobject with its own overrider.
Line 3`Base& base = b;` compiles because there is exactly one Base subobject to bind to.
Line 4Remove both `virtual` keywords and this file stops compiling: `b.ping()` and the reference binding both become ambiguous.
Important notes
Ambiguity is decided during name lookup, before access checking and before overload resolution, so a `send` that is private in one base still takes part and still makes the unqualified call ambiguous.
Construction order is fixed when virtual bases are involved: all virtual bases of the complete object first, in depth-first left-to-right order of the base lists, then the non-virtual bases in declaration order, then the class body.
Common mistakes
Writing `virtual` on only one branch, as in `Scanner : virtual Device` with `Printer : Device`: the object then holds one shared Device and one branch-private Device, `c.id` is still ambiguous, and the fix looks like it did nothing.
Switching to virtual inheritance but leaving the `Device(...)` mem-initializer out of the most-derived class: Device gets default-constructed instead, or the code fails to compile if Device has no default constructor, while the `Device(1)` in Scanner that you expected to run is silently ignored.
Trying `static_cast<Copier*>(p)` on a `Device*` that points into a virtual base: it is ill-formed, and papering over it with a C-style cast or `reinterpret_cast` yields a pointer at the wrong offset and undefined behaviour. `dynamic_cast` is the only valid downcast from a virtual base.
Try it yourself
Change, predict, then run
Start from the non-virtual Copier hierarchy, add `void report() const` to Device that prints its id, and call it as `c.Scanner::report()` and `c.Printer::report()` to see 1 and 2. Then put `virtual` on both base specifiers, add `Copier() : Device(9) {}`, and confirm both calls now print 9 and that plain `c.report()` compiles.
Open the C++ workspaceCheck your understanding
Scanner and Printer both inherit `virtual Device` and their constructors list `Device(1)` and `Device(2)`; Copier derives from both and lists `Device(9)`. What is `c.id` for `Copier c;`, and why?
- 9, because the most-derived class initializes the shared virtual base and the middle classes' Device initializers are ignored
- 1, because Scanner is the first base listed, so its Device initializer runs and the later ones are dropped
- 2, because Printer is constructed after Scanner and overwrites the shared subobject
- It does not compile, because a virtual base with several initializers in the hierarchy is ambiguous
Show answer
The shared Device belongs to the complete object, so exactly one constructor call is made for it and Copier makes it, before Scanner and Printer are constructed at all. Option 2 is tempting if you picture construction as three successive writes to one field in base-list order, but Device is constructed once rather than overwritten; Scanner's `Device(1)` is only used when a Scanner is itself the most-derived object.