Object-Oriented Programming (C++)

Chapter 6
Inheritance and Runtime Polymorphism

Bachelors Program in Computer Science Engineering
Object-Oriented Programming (C++)   •   Bachelor

Inheritance and Runtime Polymorphism

Introduction

Object-oriented software often contains families of related concepts. A payment system may support cards, bank transfers, and digital wallets; a graphics program may manipulate circles, rectangles, and triangles; a university system may represent students, instructors, and administrators. Inheritance can express commonality among such classes, while polymorphism allows one interface to work with many implementations.

Inheritance is powerful but not automatically good design. A derived class inherits implementation and interface from a base class, yet it must also satisfy the behavioral expectations associated with that base class. This relationship is the foundation of substitutability and the Liskov Substitution Principle (LSP). Runtime polymorphism, implemented primarily through virtual functions, enables dynamic binding: the operation executed depends on the object’s actual type rather than merely on the type of the pointer or reference used to access it.

This chapter develops single and multiple inheritance, access modes, overriding, virtual functions, abstract classes, interfaces, virtual destructors, dynamic binding, slicing, casting, fragile hierarchies, and principled inheritance design in modern C++.

Learning Objectives

After completing this chapter, you should be able to:

  • explain the purpose and mechanics of inheritance in C++;
  • distinguish public, protected, and private inheritance;
  • implement and evaluate single and multiple inheritance;
  • override virtual functions correctly using override;
  • design abstract classes and interface-like types;
  • explain dynamic binding, virtual destructors, and object lifetime;
  • identify slicing, unsafe casts, and fragile base-class problems;
  • apply substitutability and the Liskov Substitution Principle;
  • select composition or inheritance using maintainability and behavioral criteria.

1. Inheritance as a Type Relationship

A class Derived inherits from Base when a derived object includes a base-class subobject and may reuse or specialize the base interface and implementation.

class Vehicle {
public:
    void start() { /* common operation */ }
};

class Car : public Vehicle {
public:
    void openTrunk() { /* car-specific operation */ }
};

A Car object is also a Vehicle object in the type-system sense. Therefore, a pointer or reference to Vehicle can refer to a Car object:

Car car;
Vehicle& v = car;
v.start();

This conversion from derived reference to base reference is an implicit upcast. It is safe because the derived object contains a valid base subobject. The reverse conversion is not generally safe: a Vehicle object need not be a Car.

Inheritance has two distinct roles:

  1. Interface inheritance: the derived class conforms to an operation set promised by the base class.
  2. Implementation inheritance: the derived class reuses data members or function implementations.

These roles should not be confused. A class may need a common interface without needing implementation reuse. In that case, an abstract interface or composition may be preferable.

1.1 Single Inheritance

Single inheritance gives a class one direct base class:

class Employee {
public:
    virtual double salary() const = 0;
    virtual ~Employee() = default;
};

class SalariedEmployee : public Employee {
private:
    double monthlySalary;
public:
    explicit SalariedEmployee(double salary) : monthlySalary(salary) {}
    double salary() const override { return monthlySalary; }
};

The derived class automatically contains an Employee subobject. Construction proceeds from the base subobject to the derived part; destruction proceeds in the reverse order.

1.2 Multiple Inheritance

Multiple inheritance gives a class more than one direct base:

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

class Serializable {
public:
    virtual std::string serialize() const = 0;
    virtual ~Serializable() = default;
};

class Report : public Printable, public Serializable {
public:
    void print() const override { /* display report */ }
    std::string serialize() const override { return "report-data"; }
};

This form is often useful when the bases are small, independent interfaces. It becomes more difficult when bases contain overlapping state or implementation. Ambiguity, duplicated base subobjects, and complicated initialization can result.

The diamond problem occurs when two intermediate classes inherit from one common base and a final class inherits from both intermediate classes:

class Person { };
class Student : public Person { };
class Employee : public Person { };
class TeachingAssistant : public Student, public Employee { };

TeachingAssistant contains two Person subobjects. If one logical Person is intended, virtual inheritance can be used:

class Person { };
class Student : virtual public Person { };
class Employee : virtual public Person { };
class TeachingAssistant : public Student, public Employee { };

With virtual inheritance, the most-derived class, TeachingAssistant, is responsible for constructing the shared virtual base. Virtual inheritance solves a structural duplication problem, but it increases conceptual and initialization complexity and should be used deliberately.

2. Access Control and Inheritance Modes

C++ has three member-access levels:

  • public: accessible through the public interface wherever the object is accessible;
  • protected: accessible in the class and its derived classes, but not by ordinary clients;
  • private: accessible only within the declaring class and its friends.

The inheritance mode modifies how inherited public and protected members appear through the derived type.

Inheritance modeBase public members becomeBase protected members becomeBase private members
publicpublicprotectedinaccessible directly
protectedprotectedprotectedinaccessible directly
privateprivateprivateinaccessible directly

Example:

class Base {
public:
    void publicOperation();
protected:
    int protectedValue{};
private:
    int privateValue{};
};

class PublicChild : public Base { };
class ImplementationChild : private Base { };

Public inheritance expresses an “is-a” relationship: a PublicChild can be used wherever a Base is expected. Private inheritance usually expresses an implementation technique: clients of ImplementationChild cannot treat it as a publicly substitutable Base. In many such cases, composition is clearer:

class Service {
private:
    Base helper;       // composition
};

A derived class cannot directly access private base members. It must use protected or public functions supplied by the base. Excessive protected state is risky because it couples the base implementation to every derived class. Prefer protected operations with clear invariants over protected data members.

3. Overriding, Hiding, and Virtual Functions

A virtual function declares that derived classes may provide a specialized implementation selected at runtime.

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override { return width * height; }
};

The override specifier asks the compiler to verify that the function actually overrides a virtual base function. It catches errors involving a misspelled name, different parameter types, missing const, or a different ref-qualification.

class Base {
public:
    virtual void process(int) const;
};

class Derived : public Base {
public:
    void process(int) const override;   // correct
};

Without override, a small signature difference could create a new function rather than a replacement.

A derived declaration can also hide all base overloads with the same name:

class Base {
public:
    virtual void write(int);
    virtual void write(double);
};

class Derived : public Base {
public:
    using Base::write;
    void write(int) override;
};

The using declaration makes the other overload visible in Derived.

3.1 What Virtual Dispatch Does and Does Not Do

Consider:

void display(const Shape& shape) {
    std::cout << shape.area();
}

When display(rectangle) is called, Rectangle::area() executes because the expression is a reference to a polymorphic object. The call is dynamically bound.

Virtual dispatch applies through a base pointer or reference. It does not apply to ordinary objects copied by value, and it does not apply to static, non-virtual functions in the same manner. Constructors and destructors also have special dispatch rules: virtual calls made during construction or destruction do not dispatch to a not-yet-constructed or already-destroyed derived part.

A pure virtual function is declared with = 0:

virtual double area() const = 0;

A class containing at least one pure virtual function is abstract and cannot be instantiated. It may still have data members, constructors, ordinary member functions, and even a definition for a pure virtual function, although a pure virtual destructor must still have a definition if it is odr-used during destruction.

4. Abstract Classes and Interfaces

An abstract base class defines a contract rather than a complete concrete object. A useful abstract class often contains a virtual destructor and operations that represent stable responsibilities:

class PaymentMethod {
public:
    virtual bool authorize(double amount) = 0;
    virtual std::string name() const = 0;
    virtual ~PaymentMethod() = default;
};

An interface-like class in C++ commonly has only public pure virtual functions, a virtual destructor, and no meaningful object state. C++ has no special interface keyword. The convention is established by class design.

Abstract interfaces support dependency inversion. A checkout service can depend on PaymentMethod, while concrete card and wallet classes can vary independently:

void checkout(PaymentMethod& method, double amount) {
    if (method.authorize(amount)) {
        // complete order
    }
}

The interface should be cohesive. An interface containing unrelated functions forces derived classes to implement operations they do not meaningfully support, often resulting in exceptions or dummy return values. Such a design violates substitutability.

5. Virtual Destructors and Ownership

If a class is intended to be used polymorphically, its destructor should generally be virtual:

class Resource {
public:
    virtual ~Resource() = default;
};

class FileResource : public Resource {
public:
    ~FileResource() override {
        // release file-specific resource
    }
};

This is essential when deleting a derived object through a base pointer:

Resource* p = new FileResource;
delete p;       // correct when Resource::~Resource is virtual

If the base destructor is non-virtual, deleting through Resource* produces undefined behavior. The derived destructor may not run, causing leaks or incomplete cleanup.

Modern C++ should usually avoid manual new and delete:

std::unique_ptr<Resource> p = std::make_unique<FileResource>();

A std::unique_ptr<Resource> still requires a virtual base destructor for correct polymorphic destruction. A class that is not intended to be deleted polymorphically may use a protected non-virtual destructor, but that is a deliberate restriction and should be documented.

6. Worked Example: A Polymorphic Notification System

Suppose an application sends notifications through email, SMS, and an internal dashboard. The application should not contain a conditional chain that tests every notification type. Instead, it can depend on an abstract contract.

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Notification {
public:
    virtual void send(const std::string& recipient,
                      const std::string& message) = 0;
    virtual ~Notification() = default;
};

class EmailNotification : public Notification {
public:
    void send(const std::string& recipient,
              const std::string& message) override {
        std::cout << "Email to " << recipient << ": " << message << '\n';
    }
};

class SmsNotification : public Notification {
public:
    void send(const std::string& recipient,
              const std::string& message) override {
        std::cout << "SMS to " << recipient << ": " << message << '\n';
    }
};

void notify(Notification& channel,
            const std::string& recipient,
            const std::string& message) {
    channel.send(recipient, message);
}

The function notify is closed to changes in the sense that adding a new notification type does not require changing its algorithm. A new PushNotification class can satisfy the same interface. The base reference does not own the object, so lifetime remains with the caller. If a collection must own heterogeneous notification objects, use polymorphic smart pointers:

std::vector<std::unique_ptr<Notification>> channels;
channels.push_back(std::make_unique<EmailNotification>());
channels.push_back(std::make_unique<SmsNotification>());

This example illustrates interface inheritance, dynamic binding, virtual destruction, dependency inversion, and extensibility.

7. Object Slicing

Object slicing occurs when a derived object is copied into a base object by value:

class Base {
public:
    virtual void show() const { std::cout << "Base"; }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void show() const override { std::cout << "Derived"; }
};

Derived d;
Base b = d;       // derived part is sliced
b.show();         // Base::show()

The new object b is a genuine Base object; it no longer contains the derived state. To preserve polymorphism, pass by reference or pointer:

void showObject(const Base& object) {
    object.show();
}

Containers of polymorphic objects should therefore generally contain pointers or smart pointers, not base objects by value. Slicing is not always an error: copying a derived object into a deliberately independent base-value representation may be appropriate, but it must be intentional.

8. Casting in an Inheritance Hierarchy

An upcast is safe and implicit:

Derived d;
Base* base = &d;

A downcast attempts to recover a derived type from a base expression. dynamic_cast performs a checked runtime cast when the base is polymorphic:

if (Derived* derived = dynamic_cast<Derived*>(base)) {
    derived->specialOperation();
}

For references, failure throws std::bad_cast:

try {
    Derived& derived = dynamic_cast<Derived&>(baseReference);
} catch (const std::bad_cast&) {
    // object is not a Derived
}

static_cast<Derived*>(base) does not perform a runtime check. It is valid only when the programmer has already established that the object is actually a suitable Derived; otherwise behavior is undefined. reinterpret_cast should not be used to navigate ordinary class hierarchies.

Frequent downcasting often indicates that the base interface is incomplete or that inheritance is being used where composition or a variant-based design would be clearer. Casting is a tool, not a substitute for sound abstraction.

9. Substitutability and the Liskov Substitution Principle

The Liskov Substitution Principle states that objects of a subtype should be usable wherever objects of the base type are expected without invalidating the correctness of the client program. In practical terms, a derived class must honor the base class’s observable contract.

A useful contract analysis considers:

  • Preconditions: what must be true before an operation;
  • Postconditions: what the operation guarantees afterward;
  • Invariants: properties that remain true for valid objects;
  • Observable behavior: results, exceptions, side effects, and resource guarantees.

A subtype should not require stronger preconditions than the base contract. It should provide at least the promised postconditions and should not unexpectedly weaken important guarantees. For example, if a base Collection::remove(x) promises that removal of an existing item succeeds, a subtype that always throws for such a request is not substitutable.

The classic rectangle-square example illustrates the issue. If Rectangle permits width and height to change independently, then a Square subtype that forces both dimensions to change together may violate client expectations. The problem is not mathematical classification; it is incompatible behavior under the software contract.

LSP does not prohibit all specialization. A derived class may strengthen internal algorithms, improve performance, or add operations, provided the inherited contract remains valid.

10. Fragile Base Classes and Design Risks

A fragile base-class problem occurs when a change to a base class unexpectedly breaks derived classes. Causes include:

  • derived classes depending on undocumented implementation details;
  • base constructors calling overridable functions;
  • protected data being directly manipulated by subclasses;
  • adding overloads that hide derived functions;
  • changing virtual function semantics without updating the contract;
  • assuming all subclasses share one invariant.

Avoid calling virtual functions from constructors and destructors. During base construction, the derived portion does not yet exist; during base destruction, it has already been destroyed. Prefer non-virtual private helper functions or explicit initialization steps.

The Non-Virtual Interface pattern can stabilize contracts:

class Document {
public:
    void save() {
        validate();
        doSave();
    }

    virtual ~Document() = default;

private:
    void validate() const { /* invariant checks */ }
    virtual void doSave() = 0;
};

The public non-virtual function controls the algorithm and invariant checks, while a protected or private virtual customization point supplies variation. This is useful, but it must be applied carefully because derived implementations still need a documented contract.

11. Inheritance or Composition?

Inheritance is appropriate when all of the following are substantially true:

  1. the subtype genuinely represents a specialization of the base abstraction;
  2. clients can use the subtype through the base contract;
  3. the base interface is stable and cohesive;
  4. polymorphic variation is required or implementation reuse is clearly valuable.

Composition is often better when one class merely uses another, when behavior may change independently, or when the relationship is “has-a” rather than “is-a.” Composition reduces coupling and avoids inherited interface obligations.

A practical design process is:

  1. write the client operations that must remain stable;
  2. state preconditions, postconditions, and invariants;
  3. test candidate subtypes against those contracts;
  4. keep base interfaces minimal;
  5. prefer references, smart pointers, and ownership clarity;
  6. use override, final, and const to make intent explicit;
  7. test destruction through base pointers;
  8. document exception and resource behavior.

The final specifier can prevent further derivation or overriding:

class SecureToken final { };
class Base { virtual void f(); };
class Derived : public Base { void f() override final; };

It is useful when extension would violate invariants or when a hierarchy is intentionally closed.

12. Applied Case: A University Transport System

This is an illustrative case.

Background

A university operates buses, electric shuttles, and contracted taxis. The scheduling application initially uses a large conditional statement based on vehicle codes. Every new vehicle type requires changes to scheduling, reporting, and maintenance modules.

Problem

The system needs a stable way to request capacity, begin a trip, and report maintenance. However, vehicles differ in energy systems and operational rules. A design that exposes every vehicle-specific detail to clients would create tight coupling.

Analysis

The stable concept is an operational vehicle, not a collection of implementation details. The base contract can define capacity(), startTrip(), and maintenanceStatus(). It should not promise that every vehicle has a fuel tank or that every vehicle can operate on every route.

Approach and Solution

class CampusVehicle {
public:
    virtual int capacity() const = 0;
    virtual void startTrip() = 0;
    virtual bool needsMaintenance() const = 0;
    virtual ~CampusVehicle() = default;
};

class ElectricShuttle : public CampusVehicle {
private:
    int seats;
    int batteryPercent;
public:
    ElectricShuttle(int s, int battery)
        : seats(s), batteryPercent(battery) {}

    int capacity() const override { return seats; }
    void startTrip() override {
        if (batteryPercent >= 20) --batteryPercent;
    }
    bool needsMaintenance() const override {
        return batteryPercent < 15;
    }
};

The scheduling service can accept CampusVehicle& and remain independent of battery-specific implementation. A taxi adapter can implement the same interface while translating calls to a contractor’s API. Multiple inheritance could be used if a vehicle also implements independent interfaces such as Trackable and Reportable, but shared state should be introduced only when its ownership and invariants are clear.

Outcome

New vehicle types can be added with limited changes. Unit tests can substitute a test vehicle implementing the same contract. Polymorphic ownership can be represented by std::unique_ptr<CampusVehicle>, and the virtual destructor ensures correct cleanup.

Learning Points

The successful design is not based merely on common nouns. It is based on a stable behavioral contract, minimal interface, explicit ownership, and substitutable implementations. If a vehicle cannot satisfy an operation meaningfully, that operation does not belong in the common base interface.

13. Contemporary C++ and Professional Practice

Modern C++ combines inheritance with other abstraction tools. Concepts and templates provide compile-time polymorphism; std::variant represents a closed set of alternatives; type erasure supports runtime polymorphism without exposing a conventional inheritance hierarchy. These alternatives can reduce virtual-call overhead or make permitted types explicit, but they do not eliminate the need for behavioral contracts.

Virtual dispatch usually introduces an indirect call and commonly requires per-object or per-class runtime metadata. The performance cost is often insignificant compared with I/O or complex computation, but it may matter in tightly constrained real-time or cache-sensitive systems. Optimization should be evidence-based through profiling rather than assumed from the presence of virtual.

Security and reliability considerations include validating plugin implementations, preventing unauthorized substitution of resource-handling classes, and ensuring exception-safe destruction. Public polymorphic interfaces should specify ownership, thread-safety expectations, error reporting, and lifetime requirements. In safety-critical software, inheritance may be restricted because uncontrolled extension complicates verification.

Chapter Summary

Inheritance models relationships in which a derived object contains a base subobject and may specialize its interface or implementation. Public inheritance should communicate a genuine, behaviorally valid “is-a” relationship; private inheritance is primarily an implementation mechanism, and composition is often clearer.

Virtual functions enable runtime polymorphism through base pointers and references. Pure virtual functions create abstract classes, while interface-like classes express cohesive contracts without requiring concrete instances. A virtual destructor is essential when derived objects may be destroyed through base pointers. Slicing removes the derived portion of an object copied by value, so polymorphic collections normally use references, pointers, or smart pointers.

Safe upcasting is implicit, whereas checked downcasting can use dynamic_cast. Frequent downcasting may reveal a defective abstraction. The Liskov Substitution Principle requires derived classes to preserve base contracts, including preconditions, postconditions, invariants, and observable behavior. Fragile hierarchies are reduced through minimal interfaces, encapsulation, explicit contracts, careful construction and destruction, and a preference for composition when inheritance is not justified.

Key Terms

Inheritance: Mechanism by which a class derives interface and implementation from another class.

Base class: Class from which another class inherits.

Derived class: Class that inherits from a base class.

Single inheritance: Inheritance from one direct base class.

Multiple inheritance: Inheritance from two or more direct base classes.

Public inheritance: Inheritance that preserves public substitutability.

Protected inheritance: Inheritance that exposes inherited public and protected members as protected.

Private inheritance: Inheritance that makes inherited public and protected members private.

Virtual function: Member function selected dynamically through a polymorphic base interface.

Override: A derived implementation replacing a virtual base function.

Pure virtual function: Virtual function declared with = 0.

Abstract class: Class that cannot be instantiated, usually because it has a pure virtual function.

Interface: Cohesive contract of operations supplied to clients.

Dynamic binding: Runtime selection of an overridden virtual function.

Virtual destructor: Destructor enabling correct polymorphic destruction.

Object slicing: Loss of derived state when copying a derived object into a base object by value.

Upcast: Conversion from a derived type to a base type.

Downcast: Conversion from a base expression to a derived type.

Liskov Substitution Principle: Requirement that subtypes remain valid substitutes for their base types.

Fragile base class: Base class whose changes can unexpectedly break derived classes.

Review Questions

Short Answer Questions

  1. What is the difference between interface inheritance and implementation inheritance?
  2. Why should a polymorphic base class usually have a virtual destructor?
  3. What does the override specifier accomplish?
  4. Define object slicing and give one way to avoid it.
  5. When is public inheritance preferable to private inheritance?

Long Answer Questions

  1. Explain single and multiple inheritance in C++, including the diamond problem and virtual inheritance.
  2. Describe how virtual functions, base references, and dynamic binding work together.
  3. Explain abstract classes and interface-like classes and discuss principles for designing cohesive interfaces.
  4. Discuss the Liskov Substitution Principle using preconditions, postconditions, and invariants.
  5. Compare inheritance and composition, including maintainability, coupling, and extensibility.

Analytical / Application Questions

  1. A derived class throws an exception for an operation that the base contract guarantees will succeed. Analyze whether the design satisfies LSP and propose a correction.
  2. Design a polymorphic class hierarchy for multiple document export formats. Identify the base contract, ownership model, destructor requirements, and possible misuse of downcasting.
  3. A program stores std::vector<Base> objects even though several derived classes exist. Explain the resulting behavior and redesign the collection to preserve polymorphism safely.

Multiple Choice Questions with Answers

Question 1

Which inheritance mode preserves the public substitutability of a derived class for its base class?

A. Public inheritance

B. Private inheritance

C. Protected inheritance

D. Virtual inheritance only

Correct Answer: A. Public inheritance

Question 2

What is the primary purpose of the override specifier?

A. To make a function static

B. To request compiler verification of a virtual override

C. To prevent dynamic binding

D. To allocate an object on the heap

Correct Answer: B. To request compiler verification of a virtual override

Question 3

Which call requires a virtual destructor for correct polymorphic destruction?

A. Deleting a Derived* directly

B. Deleting a Base* that points to a Derived object

C. Calling a static function

D. Copying a base object

Correct Answer: B. Deleting a Base* that points to a Derived object

Question 4

What happens when a derived object is copied into a base object by value?

A. The base object gains all derived functions

B. The derived portion is sliced away

C. The copy becomes a reference

D. The operation always fails to compile

Correct Answer: B. The derived portion is sliced away

Question 5

Which cast performs a runtime-checked downcast for a polymorphic hierarchy?

A. reinterpret_cast

B. C-style cast

C. dynamic_cast

D. Implicit conversion

Correct Answer: C. dynamic_cast

Question 6

A class containing a pure virtual function is generally:

A. A namespace

B. An abstract class

C. A final class automatically

D. A class with no destructor

Correct Answer: B. An abstract class

Question 7

During construction of a base subobject, a virtual call made from the base constructor dispatches to:

A. The most-derived override

B. The derived constructor

C. The current construction level, not an unconstructed derived part

D. A random override

Correct Answer: C. The current construction level, not an unconstructed derived part

Question 8

Which relationship is usually better represented by composition?

A. A genuine substitutable specialization

B. A class merely using another class as a helper

C. An abstract interface implementation

D. A virtual function override

Correct Answer: B. A class merely using another class as a helper

Question 9

The Liskov Substitution Principle primarily requires that:

A. Every class have multiple base classes

B. Derived objects preserve the behavioral expectations of the base type

C. All functions be non-virtual

D. Base classes expose protected data

Correct Answer: B. Derived objects preserve the behavioral expectations of the base type

Question 10

What is a common symptom of a fragile or poorly designed hierarchy?

A. Derived classes remain valid after every base change

B. Clients use a small stable interface

C. Derived classes depend on undocumented base implementation details

D. The base destructor is virtual

Correct Answer: C. Derived classes depend on undocumented base implementation details

Object-Oriented Programming (C++)
Chapter 6 of 15