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.
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.
// 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.
// 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.
// 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.
// 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 finalfinal 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.
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++14To 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.
// 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.
// 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.
// 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.
// 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.
// 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.