Inheritance and Class Hierarchies
C++ inheritance — single, multiple, virtual base classes, diamond problem, access specifiers, object slicing, and override/final semantics.
Inheritancesince C++98Inheritance 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
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
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: privatePrivate 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
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)
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)
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
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
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
// 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; // unambiguousVirtual 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:
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
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_uniqueSlicing 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 virtualorprotected non-virtual. Non-virtual destructors in a polymorphic base lead to undefined behaviour ondelete base_ptr. - Always use
overrideon 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 tofinaltypes. - 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
structfor 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.
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.