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.

Access specifiers in inheritance

The inheritance specifier controls how a base class's interface is exposed to the outside world, not what the derived class can access internally. All three modes (public, protected, private) leave the derived class with the same internal access to inherited members; what changes is what callers and further-derived classes see.

cpp
// C++98
struct Base {
    void pub_fn() {}
protected:
    void prot_fn() {}
};

struct PublicDerived : public Base {
    // Base::pub_fn  visible as public    — callers can call it
    // Base::prot_fn visible as protected — further derivation can call it
};

struct ProtectedDerived : protected Base {
    // Base::pub_fn  visible as protected — callers cannot call it
    // Base::prot_fn visible as protected
};

struct PrivateDerived : private Base {
    // Base::pub_fn  visible as private   — callers cannot call it
    // Base::prot_fn visible as private   — further derivation cannot call it
    void relay() { pub_fn(); }  // still OK inside PrivateDerived itself
};

Use public for genuine "is-a" relationships. Use private for "implemented-in-terms-of" (though a member variable is usually clearer). protected inheritance is rarely the right choice.

Constructor inheritance with using

using Base::Base (C++11) pulls every base constructor into the derived class scope as if the derived class had declared matching constructors itself. This is the idiomatic way to avoid boilerplate forwarding constructors.

cpp
// C++11
struct Widget {
    int  id;
    bool visible;
    explicit Widget(int i, bool v = true) : id{i}, visible{v} {}
};

struct Button : Widget {
    using Widget::Widget;       // inherit Widget(int, bool) and Widget(int)
    std::string label;          // default-initialised to "" for inherited ctors
};

Button b1{42};             // Widget(42, true)  — label == ""
Button b2{7, false};       // Widget(7,  false) — label == ""

Inherited constructors do not initialise members added in the derived class beyond their in-class default initialisers, so always provide defaults for new members when using using Base::Base.

cpp
// C++11 — caveat: no default initialiser means uninitialised member
struct Dangerous : Widget {
    using Widget::Widget;
    int extra;           // no default initialiser — indeterminate value after Widget(int)
};

Override and final keywords

override (C++11) instructs the compiler to verify that the decorated function actually overrides a virtual function in a base class. Without it, a signature mismatch silently creates a new (non-overriding) function.

cpp
// C++11
struct Sensor {
    virtual double read() const = 0;
    virtual void   calibrate(int steps) {}
    virtual ~Sensor() = default;
};

struct TempSensor : Sensor {
    double read() const override { return 36.6; }        // OK
    void   calibrate(int steps) override {}               // OK
    // void calibrate(float steps) override {}            // error: no match in Sensor
};

// C++11 — final prevents further overriding of a specific method
struct CalibratedSensor : TempSensor {
    double read() const override final { return 36.7; }  // no subclass may override read()
};

// C++11 — final on the class prevents any further derivation
struct FactorySensor final : TempSensor {
    double read() const override { return 25.0; }
};

// struct SubFactory : FactorySensor {};  // error: FactorySensor is final

final on a class lets the compiler devirtualise calls to that type's virtual functions, which can yield measurable performance gains in tight loops.

Slicing problem

Object slicing occurs when a derived object is copied or assigned into a base-type value, silently discarding all derived-class state and replacing the vtable with the base's. It is not undefined behaviour by itself, but it almost always represents a logic error.

cpp
struct Shape {
    virtual std::string describe() const { return "Shape"; }
    virtual ~Shape() = default;
};

struct Circle : Shape {
    double radius = 1.0;
    std::string describe() const override { return "Circle r=" + std::to_string(radius); }
};

void print_shape(Shape s) {              // takes by value — SLICES
    std::cout << s.describe() << '\n';   // always prints "Shape"
}

Circle c;
c.radius = 5.0;
print_shape(c);          // prints "Shape" — radius and Circle vtable are gone

// C++11 — correct approaches
void print_ref(const Shape& s)  { std::cout << s.describe() << '\n'; }  // OK
void print_ptr(const Shape* s)  { std::cout << s->describe() << '\n'; } // OK

std::vector<std::unique_ptr<Shape>> shapes;          // C++11
shapes.push_back(std::make_unique<Circle>());        // C++14

To make slicing a hard error, declare the base copy constructor and copy-assignment operator as deleted or protected. The Boost noncopyable idiom and NVI pattern both exploit this.

Protected vs private inheritance

Protected and private inheritance are not IS-A relationships; they model implementation borrowing. The derived class reuses the base's internals but does not expose the base interface to callers.

cpp
// C++98
class Stack : private std::vector<int> {    // "implemented in terms of" vector
public:
    void push(int v) { std::vector<int>::push_back(v); }
    void pop()       { std::vector<int>::pop_back(); }
    int  top() const { return std::vector<int>::back(); }
    bool empty() const { return std::vector<int>::empty(); }
    // Callers cannot call resize(), insert(), etc. — vector interface is hidden
};

The same effect is usually achievable with a private member variable, which is simpler to reason about and avoids accidental exposure if someone adds a using declaration.

cpp
// C++98 — prefer composition over private inheritance
class StackByComposition {
    std::vector<int> data_;
public:
    void push(int v) { data_.push_back(v); }
    void pop()       { data_.pop_back(); }
    int  top() const { return data_.back(); }
    bool empty() const { return data_.empty(); }
};

Private inheritance wins over composition in two narrow cases: when you need to override a virtual function in the base, or when you need to access a protected member and cannot modify the base class.

Liskov Substitution Principle in practice

LSP states that any code working with a Base* must behave correctly when handed a Derived* without knowing the difference. Violating LSP produces hierarchies that are brittle, hard to test, and prone to surprising runtime failures.

cpp
// C++11 — LSP violation: Square is NOT a substitutable Rectangle
struct Rectangle {
    virtual void set_width(int w)  { width_  = w; }
    virtual void set_height(int h) { height_ = h; }
    int area() const { return width_ * height_; }
protected:
    int width_ = 0, height_ = 0;
};

struct Square : Rectangle {
    void set_width(int w)  override { width_ = height_ = w; }   // breaks Rectangle contract
    void set_height(int h) override { width_ = height_ = h; }
};

void resize(Rectangle& r) {
    r.set_width(4);
    r.set_height(5);
    assert(r.area() == 20);   // passes for Rectangle, fails for Square
}

The fix is to not model squares as derived rectangles. Prefer separate, unrelated types that share a common abstract interface (e.g., Shape) if they genuinely need to be used polymorphically.

cpp
// C++11 — LSP-compliant design: immutable value types, no shared mutable state
struct Rectangle {
    int width, height;
    int area() const { return width * height; }
};

struct Square {
    int side;
    int area() const { return side * side; }
};

Inheritance vs composition decision guide

The core question is: does the derived type need to be substitutable everywhere the base type is accepted? If yes, public inheritance with LSP compliance is appropriate. If no, prefer composition.

cpp
// C++11 — prefer composition (has-a) when the relationship is one of use
struct Logger {
    void log(std::string_view msg) { /* ... */ }
};

struct Engine {
    void start() { logger_.log("Engine started"); }
private:
    Logger logger_;     // Engine HAS-A Logger — not IS-A
};

// C++11 — use public inheritance (is-a) only for genuine polymorphic substitution
struct Transport {
    virtual void send(std::span<const std::byte> data) = 0;   // C++20: std::span
    virtual ~Transport() = default;
};

struct TcpTransport  : public Transport { void send(std::span<const std::byte>) override {} };
struct UdpTransport  : public Transport { void send(std::span<const std::byte>) override {} };

A useful heuristic: if you would not write assert(dynamic_cast<Base*>(derived_ptr) != nullptr) and rely on the cast succeeding, the relationship is not IS-A. Additional signals favouring composition over inheritance: the base class has data you do not need, you only want one method from a large interface, or unit testing the derived class requires setting up the base class.

See Also