Skip to content
C++
Language
since C++98
Intermediate

Inheritance and Class Hierarchies

C++ inheritance — single, multiple, virtual base classes, diamond problem, access specifiers, object slicing, and override/final semantics.

Inheritancesince C++98

Inheritance is a class relationship in which a derived class acquires the data members and member functions of one or more base classes, enabling code reuse and polymorphic dispatch through base-class pointers and references.

Overview

C++ inheritance is not just a reuse mechanism — it shapes object layout, lifetime semantics, and dispatch behaviour. Three inheritance modes exist (public, protected, private), and C++ uniquely supports multiple inheritance with virtual bases to resolve ambiguity. C++11 added override and final keywords that catch override mismatches at compile time; these should be considered mandatory in modern code.

The fundamental rule: prefer composition. Use public inheritance only for genuine "is-a" relationships where Liskov substitutability holds. Use private inheritance sparingly when you need to call protected members or override virtuals without exposing the base interface.

Syntax

cpp
struct Base {
    virtual void dispatch() const = 0;   // pure virtual
    virtual ~Base() = default;           // virtual destructor — mandatory
};

// C++98: public inheritance
struct Derived : public Base {
    void dispatch() const override;      // override keyword: C++11
};

// Multiple inheritance
struct Interface1 { virtual void a() = 0; virtual ~Interface1() = default; };
struct Interface2 { virtual void b() = 0; virtual ~Interface2() = default; };

struct Concrete : public Interface1, public Interface2 {
    void a() override {}
    void b() override {}
};

// Prevent further derivation — C++11
struct Leaf final : public Base {
    void dispatch() const override final {}  // final on method: C++11
};

Examples

Access specifier semantics

cpp
struct Base {
    int pub  = 1;
protected:
    int prot = 2;
private:
    int priv = 3;   // never accessible in derived classes
};

struct PublicDerived    : public    Base {};  // pub→pub,  prot→prot
struct ProtectedDerived : protected Base {};  // pub→prot, prot→prot
struct PrivateDerived   : private   Base {};  // pub→priv, prot→priv

PublicDerived    pd; pd.pub;   // OK
ProtectedDerived rd; rd.pub;   // error: protected
PrivateDerived   vd; vd.pub;   // error: private

Private inheritance is most useful when you want to call a protected virtual in a third-party class or model "implemented-in-terms-of" without exposing the base. For anything else, prefer a member variable.

Constructor and destructor ordering

cpp
struct Base {
    Base()  { std::println("Base()");  }    // std::println: C++23
    ~Base() { std::println("~Base()"); }
};

struct Member {
    Member()  { std::println("Member()");  }
    ~Member() { std::println("~Member()"); }
};

struct Derived : Base {
    Member m;
    Derived()  { std::println("Derived()");  }
    ~Derived() { std::println("~Derived()"); }
};

// Construction: Base() → Member() → Derived()
// Destruction:  ~Derived() → ~Member() → ~Base()

Base classes are constructed before member variables. Destruction is strictly the reverse. If Base has a non-virtual destructor and you delete through a Base*, only ~Base() runs — undefined behaviour for any derived state. Always declare the base destructor virtual when the class is used polymorphically.

Calling base constructors and delegating (C++11)

cpp
struct Shape {
    std::string color;
    explicit Shape(std::string c) : color{std::move(c)} {}
};

struct Circle : Shape {
    double radius;
    Circle(double r, std::string c)
        : Shape{std::move(c)}, radius{r} {}

    // Delegating constructor — C++11
    explicit Circle(double r) : Circle{r, "black"} {}
};

Delegating constructors (C++11) allow one constructor to forward to another in the same class, avoiding duplicated init logic. Delegation and member initialisation are mutually exclusive in a single constructor definition.

Inheriting constructors (C++11)

cpp
struct Base {
    Base(int x) {}
    Base(double y, std::string s) {}
};

struct Derived : Base {
    using Base::Base;   // C++11: pulls all Base constructors into Derived
    int extra = 0;
};

Derived d1{42};
Derived d2{3.14, "hello"};

Inherited constructors do not initialise members added in Derived beyond their default initialisers. If Derived declares any constructor explicitly, only the declared ones exist unless using Base::Base is present.

Using declarations for overload sets

cpp
struct Base {
    void process(int)    { std::println("Base::process(int)");    }
    void process(double) { std::println("Base::process(double)"); }
};

struct Derived : Base {
    using Base::process;           // bring the overload set into scope
    void process(std::string) { std::println("Derived::process(string)"); }
};

Derived d;
d.process(1);       // Base::process(int)    — without using, this would be hidden
d.process(3.14);    // Base::process(double)
d.process("hi");    // Derived::process(string)

Without using Base::process, any process in Derived hides all base overloads regardless of signature — a common pitfall.

Multiple inheritance and name ambiguity

cpp
struct Logger  { void log(std::string_view msg); };
struct Metrics { void log(std::string_view msg); };   // same name, different semantic

struct Service : Logger, Metrics {
    void record(std::string_view msg) {
        Logger::log(msg);    // explicit disambiguation required
        Metrics::log(msg);
    }
};

The diamond problem and virtual inheritance

cpp
// Without virtual inheritance — two Animal subobjects in Bat
struct Animal    { int age = 0; };
struct Mammal    : Animal {};
struct WingedAnimal : Animal {};
struct Bat       : Mammal, WingedAnimal {};

Bat b;
// b.age;               // error: ambiguous
b.Mammal::age = 5;     // OK but fragile

// With virtual inheritance — one shared Animal subobject
struct VMammal       : virtual Animal {};
struct VWingedAnimal : virtual Animal {};
struct VBat          : VMammal, VWingedAnimal {};

VBat vb;
vb.age = 5;   // unambiguous

Virtual base classes carry overhead: the compiler inserts a virtual base pointer per virtual base, and the most-derived class is responsible for constructing the virtual base directly, bypassing intermediate constructors:

cpp
struct Animal {
    explicit Animal(int a) : age{a} {}
    int age;
};

struct VMammal       : virtual Animal { VMammal()       : Animal{0} {} };
struct VWingedAnimal : virtual Animal { VWingedAnimal() : Animal{0} {} };

struct VBat : VMammal, VWingedAnimal {
    VBat() : Animal{5}, VMammal{}, VWingedAnimal{} {}
    //        ^^^^^^^^ most-derived must init virtual base
};

Object slicing

cpp
struct Animal {
    std::string name;
    virtual std::string sound() const { return "..."; }
};

struct Dog : Animal {
    std::string breed;
    std::string sound() const override { return "Woof"; }
};

Dog d{"Rex", "Labrador"};

Animal a = d;        // SLICING: breed lost, vtable pointer becomes Animal's
a.sound();           // "..." — Animal::sound, not Dog::sound

Animal& ref = d;
ref.sound();         // "Woof" — polymorphism preserved through reference

// Correct polymorphic storage
std::vector<std::unique_ptr<Animal>> zoo;   // C++11: unique_ptr
zoo.push_back(std::make_unique<Dog>("Rex", "Labrador"));  // C++14: make_unique

Slicing silently truncates derived state. It occurs whenever a derived object is copied or assigned to a base value. Pass polymorphic objects by pointer or reference, and store them via std::unique_ptr<Base> or std::shared_ptr<Base>.

Best Practices

  • Declare base destructors public virtual or protected non-virtual. Non-virtual destructors in a polymorphic base lead to undefined behaviour on delete base_ptr.
  • Always use override on overriding functions (C++11). It turns ABI-silent signature mismatches into compile errors.
  • Use final (C++11) on classes that should not be subclassed — the compiler may devirtualise calls to final types.
  • Never call virtual functions from constructors or destructors. The derived vtable is not yet installed during base construction; the call dispatches to the base version, silently ignoring the override.
  • Prefer struct for interface-only base classes (pure virtual, virtual destructor, no data). This signals intent and avoids access-specifier noise.
  • Avoid deep hierarchies. Two levels (interface → concrete) is common; three or more is a design smell.

Common Pitfalls

Hiding instead of overriding. A function in a derived class with a different signature hides — does not override — the base function. override makes this a compile error instead of a runtime surprise.

cpp
struct Base {
    virtual void tick(int ms) {}
};

struct Derived : Base {
    void tick(float ms) {}       // hides Base::tick, does NOT override — silent bug
    void tick(float ms) override {} // error: no matching virtual — caught at compile time
};

Virtual base constructor responsibility. In a diamond hierarchy, the most-derived class must initialise the virtual base directly. If it omits it, the virtual base's default constructor runs — which may be wrong or may not exist.

Protected data members. Exposing data as protected couples every derived class to the representation. Expose protected functions instead; keep data private.

Non-virtual destructor in an interface. Even "pure interface" classes with no data need a virtual destructor if any client code calls delete through the base pointer.

See Also