C++ Multiple Inheritance and Virtual Bases

Use C++ multiple inheritance for interfaces, understand diamond subobjects, initialize shared virtual bases, and recognize when composition is clearer.

Lesson 38 of 44Intermediate10 min read

A type can implement more than one capability, such as printable output and a stable identifier. Multiple inheritance can express those separate interfaces. It can also combine stateful bases, where repeated ancestry introduces a diamond and potentially more than one copy of common base state. Those are different design situations and deserve different explanations.

This guide uses C++17. You will implement two small abstract interfaces, then construct a shared virtual base through a diamond. The examples state which class owns initialization and avoid assuming that similarly named members automatically merge. Composition remains a useful alternative when the relationship describes owned parts rather than substitutable capabilities.

Distinct interfaces can express distinct capabilities

A class can publicly derive from several abstract bases and implement their required functions. Each base describes an interface through which a caller can use the object. This is useful when the capabilities are independent and the derived object genuinely satisfies both contracts.

Each interface should have an appropriate virtual destructor if objects may be deleted through its base pointer. A public inheritance relationship expresses substitutability, not simply convenient code reuse. Keep interfaces small and avoid embedding unrelated mutable state in every base. If two bases define a function with the same signature, the derived override can satisfy both only when the semantics actually agree. Matching syntax is not proof that both behavioral contracts are identical.

Base subobjects are real parts of the object

Ordinary nonvirtual inheritance creates a base subobject for each relevant inheritance path. In a diamond, two intermediate bases each deriving from the same common base can contribute two separate common-base subobjects. Converting the final object to that common base can be ambiguous.

Qualified access can identify one path, but does not unify the two states. If each common base stores an identifier, those identifiers can differ. Ask whether two copies are meaningful or an accidental model. A compiler ambiguity is often helpful evidence of an unclear ownership or identity rule. Do not merely cast through one path without deciding what that path means for the application.

Virtual inheritance shares the repeated base

Declaring the common base as virtual in the intermediate inheritance paths allows the most-derived object to contain one shared applicable common-base subobject. This is about base sharing, not virtual function dispatch. The two uses of virtual solve different problems.

The most-derived class initializes a virtual base. Intermediate constructor initializers are relevant when those intermediate classes themselves are most-derived, but do not override the final class's initialization responsibility. A virtual base with no default constructor must therefore be initialized appropriately by the final type. Keep construction order in mind: actual base initialization follows language rules, not the textual order you happen to choose in the initializer list.

Resolve ambiguous members intentionally

Two bases can expose the same member name even without a diamond. The derived class can provide a deliberate wrapper or override, or the caller can qualify a base path where that choice is part of the contract. Avoid a hidden arbitrary choice that changes behavior depending on which interface a variable uses.

Overload hiding also matters: introducing a derived member with a name can hide base overloads. Appropriate using declarations can expose intended overloads, but they do not resolve incompatible behavioral responsibilities. Keep the final interface understandable to a caller who does not know every base. If inheritance makes ordinary use require repeated qualifications and casts, reconsider whether composition would communicate the relationships more clearly.

Prefer composition for owned parts

An invoice containing an address and a list of lines usually has those parts; it is not an address or a list. Composition expresses that ownership naturally and avoids mixing their public interfaces. Multiple inheritance is clearer for independent substitutable capabilities or a carefully designed shared hierarchy.

Virtual inheritance can add implementation complexity and affect layout, so do not introduce it merely to reuse a few fields. The language supports the mechanism, but a good model still needs clear identity, construction and ownership rules. Review whether a shared base really represents one entity and whether the intermediate classes can have sensible standalone instances.

Test through every promised interface

Exercise the derived object through each base interface and verify consistent behavior. For a diamond, test that the intended shared identity is initialized by the final class and that relevant conversions are unambiguous. Test destruction through a supported owning base pointer when polymorphic deletion is part of the design.

Do not rely on a presumed memory layout or manually calculate base offsets. Use the language's conversions and virtual dispatch. If serialization is required, define a stable schema rather than writing the raw object bytes. Multiple inheritance changes representation details, but the public behavior and serialization contract should remain deliberate.

Official references

C++ Multiple Inheritance and Virtual Bases Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Implement two independent interfaces

The complete program treats one report as both printable and identified. Neither interface contains shared mutable business state.

The interface references identify different capabilities of the same live object. Their lifetime is borrowed from report, which remains alive through both calls.

C++
#include <iostream>

class Printable {
public:
    virtual void print(std::ostream& output) const = 0;
    virtual ~Printable() = default;
};

class Identified {
public:
    virtual int id() const noexcept = 0;
    virtual ~Identified() = default;
};

class Report final : public Printable, public Identified {
    int id_;
public:
    explicit Report(int id) : id_(id) { }
    int id() const noexcept override { return id_; }
    void print(std::ostream& output) const override {
        output << "Report " << id_;
    }
};

int main() {
    Report report(7);
    const Printable& printable = report;
    const Identified& identified = report;
    printable.print(std::cout);
    std::cout << '\n';
    std::cout << "ID: " << identified.id() << '\n';
}
Output
Report 7
ID: 7

C++ Multiple Inheritance and Virtual Bases in Real Projects

Worked exercise: initialize one shared identity

The task is to combine a name and tag while keeping one common Entity identity. Both intermediate bases virtually inherit Entity. The final Combined class supplies the actual shared identifier. The program compares the two paths' Entity pointers to demonstrate that they reach the same subobject.

C++
#include <iostream>
#include <string>
#include <utility>

class Entity {
    int id_;
public:
    explicit Entity(int id) : id_(id) { }
    int id() const noexcept { return id_; }
    virtual ~Entity() = default;
};

class Named : virtual public Entity {
    std::string name_;
public:
    explicit Named(std::string name)
        : Entity(0), name_(std::move(name)) { }
    const std::string& name() const noexcept { return name_; }
};

class Tagged : virtual public Entity {
    std::string tag_;
public:
    explicit Tagged(std::string tag)
        : Entity(0), tag_(std::move(tag)) { }
    const std::string& tag() const noexcept { return tag_; }
};

class Combined final : public Named, public Tagged {
public:
    Combined(int id, std::string name, std::string tag)
        : Entity(id), Named(std::move(name)), Tagged(std::move(tag)) { }
};

int main() {
    Combined item(7, "Bolt", "urgent");
    Entity* from_name = static_cast<Named*>(&item);
    Entity* from_tag = static_cast<Tagged*>(&item);
    std::cout << "ID: " << item.id() << '\n';
    std::cout << std::boolalpha
              << "Shared base: " << (from_name == from_tag) << '\n';
    std::cout << item.name() << ": " << item.tag() << '\n';
}
Output
ID: 7
Shared base: true
Bolt: urgent

The zero initializers in the intermediate constructors are not used to choose the shared identity of Combined. The final initializer supplies seven. Constructing Named alone would use its own Entity initializer.

C++ Multiple Inheritance and Virtual Bases FAQ

Is virtual inheritance the same as virtual functions?

No. Virtual inheritance controls whether an applicable repeated base subobject is shared in a hierarchy. Virtual functions control dynamic dispatch through a base interface. A class can use either mechanism independently. Keep the distinction visible when explaining a diamond: adding virtual to a function does not remove duplicated base state, and sharing a base does not automatically define every override.

Who initializes a virtual base?

The most-derived class does. Intermediate classes provide initialization when they are themselves most-derived, but their initializers do not choose the shared base's state inside a further-derived object. A final class must supply an appropriate initializer when the virtual base lacks a usable default. Test that state explicitly rather than assuming the initializer list's textual order controls ownership.

Should I use multiple inheritance for every reusable component?

No. Reusable implementation parts often fit composition better. Public inheritance should express a real substitutable capability. Distinct small abstract interfaces can be appropriate, while several stateful bases can introduce conflicting invariants and construction responsibilities. Choose the model from the relationship and public contract, not simply from the desire to avoid repeating a few lines.

Can I assume where base subobjects sit in memory?

No portable application logic should depend on an invented layout. Use supported conversions, virtual calls and explicit serialized formats. Compiler and ABI choices can affect representation. Raw object bytes are not a stable storage schema. Tests should verify public behavior and permitted conversions rather than numeric pointer offsets between unrelated-looking parts.

C++ Multiple Inheritance and Virtual Bases Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

An invoice contains an address but is not substitutable for an address. Which relationship is usually clearer?

C++ Multiple Inheritance and Virtual Bases Quiz

2 questions to check what you learned.

  1. What does ordinary nonvirtual diamond inheritance commonly create?

    • Separate common-base subobjects along the relevant paths.
    • One automatically shared common base.
    • A runtime error before construction in every case.
    • No base state at all.
  2. Which class initializes a shared virtual base in a final object?

    • Whichever intermediate constructor appears last in source.
    • The most-derived class.
    • The first caller that uses a base reference.
    • Every intermediate class independently creates a shared copy.