C++ Diamond Problem: Ambiguity, Duplicate State and Virtual Inheritance
See why a C++ diamond duplicates base state, how virtual inheritance shares it, and when qualification, interfaces, or composition is the clearer design.
KnowledgeGate Team
Exam prep & CS education

The C++ diamond problem appears the moment one class inherits the same base along two routes. Here ComboMachine inherits through both Scanner and Printer, so combo.id is ambiguous and the object can contain two Device states. The non-virtual version exposes duplicate state; virtual inheritance creates one shared base, and the design rule depends on whether the model needs one identity or two. The Coding & Skill Development Courses page provides broader C++ study context.
What the C++ diamond problem actually creates
The hierarchy is Device as the top base, Scanner and Printer as the middle classes, and ComboMachine as the derived class. The routes are ComboMachine -> Scanner -> Device and ComboMachine -> Printer -> Device.
Name lookup is ambiguous because id and show() are reachable along both routes. Ordinary inheritance also places two distinct Device base subobjects inside one ComboMachine.
Ask whether the final object represents two independent Device identities or one shared identity. For two, selecting a path may suffice. For one, use a shared virtual base. Review the foundations first in the C++ Tutorial if needed.

Run the non-virtual diamond and expose both base states
Save this complete program as diamond.cpp. The ambiguous expressions remain commented, so the program itself builds.
#include <iostream>
class Device {
public:
explicit Device(int value) : id{value} {}
int id;
void show() const { std::cout << id << '\n'; }
};
class Scanner : public Device {
public:
Scanner() : Device{101} {}
};
class Printer : public Device {
public:
Printer() : Device{202} {}
};
class ComboMachine : public Scanner, public Printer {};
int main() {
ComboMachine combo;
// std::cout << combo.id << '\n';
// combo.show();
std::cout << static_cast<Scanner&>(combo).id << ' '
<< static_cast<Printer&>(combo).id << '\n';
combo.Scanner::id = 111;
combo.Printer::id = 222;
combo.Scanner::show();
combo.Printer::show();
}Scanner creates Device{101}, while Printer creates a separate Device{202}. Neither route is preferred, so the compiler rejects both commented expressions. The ambiguity is structural, not a visibility problem: both members are public.
Build and run it with:
g++ -std=c++17 -Wall -Wextra -pedantic diamond.cpp -o diamond && ./diamondThe successful output is:
101 202
111
222The first line exposes both initial values. The assignments change only their respective paths, and the calls confirm the separation. Qualification selects a state but does not combine the states. Uncomment either ambiguous line to observe a compile-time failure; wording varies by compiler.
What qualification fixes, and what it leaves broken
Scope qualification chooses a route: combo.Scanner::show() selects the scanner, while combo.Printer::show() selects the printer. Use this only when two Device subobjects with values such as 111 and 222 are intentional.
The same ambiguity affects conversion:
// Device* direct = &combo; // Ambiguous
Device* throughScanner = static_cast<Scanner*>(&combo);
Device* throughPrinter = static_cast<Printer*>(&combo);Each conversion first reaches an unambiguous middle base and then that route's Device. Here, the IDs are 111 and 222, and the pointers compare false. If the domain requires one identity, repeated qualification merely preserves duplicate state and path-dependent conversion.
Use virtual inheritance when both paths must share one base
Make both middle classes inherit Device virtually. Their initialisers matter when either class is constructed alone. For a ComboMachine, the most-derived constructor owns the shared base and chooses Device{303}. Both routes now converge on one subobject.
#include <iostream>
class Device {
public:
explicit Device(int value) : id{value} {
std::cout << "Device(" << id << ")\n";
}
int id;
void show() const { std::cout << id << '\n'; }
};
class Scanner : virtual public Device {
public:
Scanner() : Device{101} { std::cout << "Scanner\n"; }
};
class Printer : virtual public Device {
public:
Printer() : Device{202} { std::cout << "Printer\n"; }
};
class ComboMachine : public Scanner, public Printer {
public:
ComboMachine() : Device{303}, Scanner{}, Printer{} {
std::cout << "ComboMachine\n";
}
};
int main() {
ComboMachine combo;
std::cout << combo.id << '\n';
combo.show();
Device* direct = &combo;
Device* viaScanner = static_cast<Scanner*>(&combo);
Device* viaPrinter = static_cast<Printer*>(&combo);
std::cout << std::boolalpha << (viaScanner == viaPrinter) << '\n';
std::cout << direct->id << ' ' << viaScanner->id << ' '
<< viaPrinter->id << '\n';
}Construction prints Device(303), Scanner, Printer, and ComboMachine, in that order. The intermediate 101 and 202 initialisers do not create extra bases because ComboMachine owns virtual-base construction.
Access then prints 303, 303, true, and 303 303 303. Direct access is unambiguous, and conversions through Scanner* and Printer* reach the same address. Every pointer sees 303. If Scanner scanner; is constructed alone, Scanner is most-derived and Device{101} applies.
Locating a shared virtual base can make layout and access more complex. The exact size cost and object layout are implementation specific, so never depend on them.

Virtual inheritance does not resolve every name collision
Suppose Scanner adds mode() returning scan-600dpi, while Printer adds one returning print-24ppm. combo.id is safe because one Device exists, but combo.mode() remains ambiguous: the middle classes declared separate functions.
Qualify either choice, or give ComboMachine its own mode() returning combo-303; unqualified combo.mode() then uses it.
Virtual inheritance controls how many top-base instances exist. It cannot choose between competing middle-class operations; the final class must decide. Object Oriented Technology Explained reviews the broader inheritance context.
Trace construction, destruction, and initialisation responsibility
The ordinary and virtual diamonds produce these results:
Question | Ordinary diamond | Virtual diamond |
|---|---|---|
Number of |
|
|
Initial values |
|
|
Direct | Ambiguous | Valid |
Converted |
|
|
For the virtual object, construction runs Device(303) first, then direct bases Scanner and Printer, then ComboMachine. Destruction reverses that order: ~ComboMachine, ~Printer, ~Scanner, ~Device(303).
Only the most-derived constructor can choose the shared value. If it omits Device{303}, compilation requires an accessible default Device constructor, which this Device(int) design lacks.
Decide whether to qualify, inherit virtually, use interfaces, or compose
Technique | Use it when |
|---|---|
Qualification | Two base identities and their independent state are intentional. |
Virtual inheritance | Both paths truly share one stateful base identity. |
Narrow interfaces | Parents contribute contracts without a shared stateful ancestor. |
Composition | The final type contains capabilities, or ownership and lifetime should stay explicit. |
With composition, OfficeMachine contains ScannerEngine with dpi{600} and PrinterEngine with pagesPerMinute{24}, reporting 600 dpi, 24 ppm. No diamond exists. Alternatively, narrow Scannable and Printable interfaces contain only operations and virtual destructors; OfficeMachine implements both without shared parent data.
Review shared mutable state, ambiguous conversions, conflicting virtual-base initialisers, fragile qualification, and relationships that are really has-a. The C++ Programming Course supports broader practice. Coding for Placements covers a wider language set and competitive coding.
The short version and next coding step
Ordinary inheritance creates Device(101) and Device(202). Qualification selects one but preserves both. Virtual inheritance gives ComboMachine one Device(303), initialised by the most-derived constructor. Same-named middle-class functions still require a deliberate final-class policy.
Run both variants and verify that the base pointers compare false before virtual inheritance and true after it. Then model the 600 dpi, 24 ppm machine with interfaces and with composition, and explain which represents ownership more honestly.
Keep learning

C++ Abstract Classes and Interfaces: Build a Usable Storage Contract
Build a C++17 ScoreStore interface, swap vector and hash-backed implementations behind it, and verify both with the same runnable contract test.

C++ Virtual Functions and Polymorphism: Trace Dynamic Dispatch Step by Step
Trace a Shape hierarchy line by line to see how static type, dynamic type, final overriders, object slicing, and virtual destructors determine C++ output.

C++ unordered_map and unordered_set: Hashing, Equality and a Custom-Key Frequency Counter
Build a correct mental model for C++ unordered containers, then trace a custom-key counter through a real collision without merging distinct keys.

C++ STL Containers: Choose by Access Pattern, Mutation Cost and Ownership
Choose a C++ STL container from the workload, not from habit. Trace sequence operations, keyed counts, invalidation and object lifetime through exact examples.