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

Updated 15 Oct 20265 min read

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.

Non-virtual C++ diamond: ComboMachine holds two separate Device subobjects, id 101 through Scanner and id 202 through Printer.

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.

cpp
#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:

bash
g++ -std=c++17 -Wall -Wextra -pedantic diamond.cpp -o diamond && ./diamond

The successful output is:

Code
101 202
111
222

The 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:

cpp
// 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.

cpp
#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 diamond: Scanner and Printer share one Device with id 303, constructed first by ComboMachine.

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 Device subobjects

2

1

Initial values

101 and 202

303

Direct combo.id

Ambiguous

Valid

Converted Device* values compare

false

true

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.