Object Oriented Programming using 'C++

Chapter 6
Inheritance and Runtime Polymorphism

Post Graduate Program in Computer Science Engineering
Object Oriented Programming using 'C++   •   Post Graduate

6. Inheritance and Runtime Polymorphism

Introduction and Context

Object-oriented software is often required to represent entities that share common characteristics while differing in specialized behaviour. A payment system, for example, may process card payments, bank transfers, and digital-wallet payments through a common interface. A graphics editor may treat circles, rectangles, and polygons as shapes, even though each calculates its area and renders itself differently. Inheritance and runtime polymorphism provide C++ mechanisms for expressing such relationships.

Inheritance enables a class to acquire and extend the interface and implementation of another class. Runtime polymorphism allows a program to select an overridden operation according to the actual object type rather than the declared type of the reference or pointer used to access it. These mechanisms are powerful, but they are not automatically good design. Poorly chosen inheritance creates rigid hierarchies, hidden coupling, fragile base classes, object-slicing errors, and incorrect destruction behaviour.

This chapter therefore treats inheritance as a design decision rather than merely a language feature. It examines substitutability, abstract classes, virtual functions, overriding, dynamic binding, multiple inheritance, virtual destructors, and the alternative of composition. The central question is not simply “Can one class inherit from another?” but “Does the relationship express a stable and meaningful abstraction that clients can safely depend upon?”

Learning Objectives

After studying this chapter, the learner should be able to:

1. Explain the forms and semantics of inheritance in C++.

2. Distinguish static binding from dynamic binding and describe the role of virtual functions.

3. Design abstract classes and interfaces using pure virtual functions.

4. Apply overriding, `override`, and `final` correctly.

5. Evaluate substitutability and identify violations of the Liskov Substitution Principle.

6. Explain object slicing, virtual destructors, and safe polymorphic destruction.

7. Analyse the costs and uses of multiple inheritance, including virtual base classes.

8. Decide when composition is preferable to inheritance.

9. Apply these ideas to maintainable, testable, and extensible C++ systems.

Inheritance as a Relationship Between Types

A derived class is declared using a base class:

```cpp

class Employee {

public:

void identify() const { / common operation / }

};

class Manager : public Employee {

public:

void approveBudget() { / specialized operation / }

};

```

With public inheritance, a `Manager` is publicly usable wherever an `Employee` is expected. This expresses an “is-a” relationship. The derived class contains a base-class subobject and may add data members, member functions, and constraints of its own. A derived object is therefore larger or at least structurally richer than its base portion.

The inheritance access specifier controls how inherited public and protected members are exposed through the derived class. Public inheritance preserves the base interface’s public accessibility. Protected inheritance makes inherited public and protected members protected in the derived class, while private inheritance makes them private. These less common forms express implementation or controlled-interface relationships rather than ordinary substitutability. In application design, public inheritance should normally be reserved for a genuine behavioural subtype relationship.

A base class can expose common implementation through protected members, but protected state often increases coupling. Derived classes become dependent on representation details, making changes to the base class risky. Prefer private data with protected or public operations that preserve invariants.

Construction, Destruction, and Access

Construction proceeds from the base subobject to the derived portion; destruction proceeds in the reverse order. The base constructor establishes the base invariant before the derived constructor runs. A derived destructor executes before the base destructor. This ordering is automatic and should not be duplicated manually.

Private members of a base class are present in the base subobject but cannot be accessed directly by derived classes. Protected members are accessible to derived classes, but access should be used sparingly because it weakens encapsulation. Public members form the contractual interface available to clients.

A derived class may use a base constructor through a constructor-initializer list:

```cpp

class Employee {

public:

explicit Employee(std::string n) : name(std::move(n)) {}

private:

std::string name;

};

class Manager : public Employee {

public:

Manager(std::string n, int level)

: Employee(std::move(n)), managementLevel(level) {}

private:

int managementLevel;

};

```

If a base has no accessible default constructor, every derived constructor must explicitly select an appropriate base constructor.

Static and Dynamic Binding

Binding connects a function call with the function body that executes. Non-virtual member functions are normally resolved statically from the expression’s declared type:

```cpp

class Report {

public:

void print() const { / base version / }

};

class AuditReport : public Report {

public:

void print() const { / derived version / }

};

Report* p = new AuditReport;

p->print(); // calls Report::print()

```

The derived function here hides the base function; it does not override it because the base function is not virtual. A virtual function enables dynamic binding:

```cpp

class Report {

public:

virtual void print() const { / base version / }

virtual ~Report() = default;

};

class AuditReport : public Report {

public:

void print() const override { / derived version / }

};

Report* p = new AuditReport;

p->print(); // calls AuditReport::print()

delete p; // safe because the destructor is virtual

```

The declared type of `p` determines what operations are accessible, while the actual object type determines which virtual override runs. Dynamic binding therefore combines compile-time interface checking with runtime behaviour selection.

A virtual function is commonly implemented using compiler-generated dispatch metadata, often described through a virtual table and a hidden pointer. The C++ standard does not require this particular implementation, but the model helps explain the runtime indirection and why polymorphism can involve a small performance and memory cost. In performance-critical code, this cost should be measured rather than assumed to be unacceptable.

Overriding Correctly

An overriding function must match the relevant virtual function’s name, parameter types, cv-qualification, ref-qualification, and exception specification requirements. The `override` specifier asks the compiler to verify that overriding actually occurs:

```cpp

class Sensor {

public:

virtual double read() const = 0;

};

class TemperatureSensor : public Sensor {

public:

double read() const override { return 23.5; }

};

```

Without `override`, a small signature error can silently create a new function and leave the base implementation active. A derived class can prevent further overriding with `final`:

```cpp

class CalibratedSensor final : public Sensor {

public:

double read() const override { return 23.5; }

};

```

A derived implementation may call the base version explicitly, but ordinary calls through a base reference remain dynamically dispatched. Qualified calls such as `Base::operation()` suppress virtual dispatch for that particular call.

Function overloading and overriding are different. Overloading introduces functions with the same name but different parameter lists, usually in one scope. Overriding replaces the behaviour of an inherited virtual function with a compatible signature. Name hiding can occur when a derived class declares any function with the same name. A `using Base::operation;` declaration can restore selected base overloads when appropriate.

Abstract Classes and Contracts

An abstract class contains at least one pure virtual function, declared with `= 0`:

```cpp

class Storage {

public:

virtual void save(std::string_view key,

std::string_view value) = 0;

virtual std::string load(std::string_view key) const = 0;

virtual ~Storage() = default;

};

```

`Storage` defines an interface but cannot be instantiated directly. A concrete derived class must implement every inherited pure virtual function, unless it too remains abstract. Abstract classes may contain data, constructors, non-virtual functions, and even definitions of pure virtual functions. A pure virtual destructor must nevertheless have a definition because destruction of a derived object eventually invokes the base destructor:

```cpp

class Plugin {

public:

virtual ~Plugin() = 0;

};

Plugin::~Plugin() = default;

```

An abstract base class is useful when several implementations must conform to a common contract. It supports dependency inversion: high-level code can depend on `Storage` rather than on a particular database or file implementation. This improves substitution and testing, provided the interface is cohesive and its behavioural promises are explicit.

An interface-like class usually has a public virtual destructor, no public data, and mostly pure virtual operations. C++ does not have a separate interface keyword, so such a design is expressed by convention. A virtual destructor is essential when clients may destroy derived objects through base pointers.

Substitutability and Behavioural Design

Public inheritance should satisfy the Liskov Substitution Principle: code written for the base type should continue to work when given an object of the derived type, without surprising changes to correctness or required assumptions. This principle concerns behaviour, not merely matching method signatures.

Suppose `Rectangle` provides independently settable width and height. A mathematically attractive `Square : public Rectangle` may violate substitutability if setting the width changes the height and clients rely on independent dimensions. The problem is not that a square is unrelated to a rectangle in geometry; it is that the software contract of `Rectangle` does not match the behaviour of `Square`.

Substitutability can be examined through:

  • Preconditions: a derived operation should not require more than the base contract requires.
  • Postconditions: it should provide at least what the base contract promises.
  • Invariants: it must preserve assumptions clients are entitled to make.
  • Failure behaviour: exceptions, error states, and resource effects should remain compatible.
  • Observability: timing, mutability, ownership, and side effects may matter to clients.

A subtype may add capabilities, but clients using only the base interface must not need to know the subtype’s identity to use it correctly. If a function repeatedly performs type tests or casts to recover derived-specific behaviour, the abstraction may be inadequate or the inheritance relationship may be inappropriate.

Object Slicing and Polymorphic Access

When a derived object is copied into a base object by value, the derived portion is discarded. This is object slicing:

```cpp

class Document {

public:

virtual std::string type() const { return "document"; }

};

class Invoice : public Document {

public:

std::string type() const override { return "invoice"; }

};

Invoice invoice;

Document copy = invoice; // slices the Invoice part

copy.type(); // uses Document object semantics

```

Slicing commonly occurs when a function accepts a polymorphic base by value or when a container stores base objects by value. Use a reference or pointer for non-owning polymorphic access:

```cpp

void display(const Document& document);

std::vector<std::unique_ptr<Document>> documents;

```

If copying a polymorphic hierarchy is required, the base interface should provide a virtual cloning operation, often returning `std::unique_ptr<Base>`. Such a design must define ownership and copy semantics explicitly; simply adding a virtual destructor does not make copying polymorphic.

Virtual Destructors and Ownership

Consider:

```cpp

class Task {

public:

virtual void execute() = 0;

~Task() = default; // not virtual

};

class NetworkTask : public Task {

public:

~NetworkTask() { / release resource / }

void execute() override {}

};

Task* task = new NetworkTask;

delete task; // undefined behaviour

```

Deleting a derived object through a base pointer when the base destructor is not virtual produces undefined behaviour. The usual rule is simple: if a class has any virtual function and is intended for polymorphic use, give it a public virtual destructor, often `virtual ~Task() = default;`.

The converse design is also important. A class that is not intended to be used polymorphically may have a non-virtual destructor and should generally not be used as a public base. A protected non-virtual destructor can prevent deletion through a base pointer while permitting controlled derived use, but this is a specialized technique.

Modern C++ expresses ownership with RAII and smart pointers. `std::unique_ptr<Base>` is appropriate for exclusive ownership of a polymorphic object, provided the base destructor is virtual. `std::shared_ptr<Base>` should be used only when shared ownership is genuinely required. Raw owning pointers obscure responsibility and increase the risk of leaks and invalid deletion.

Multiple Inheritance and Virtual Bases

C++ permits a class to inherit from more than one base:

```cpp

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 {}

std::string serialize() const override { return {}; }

};

```

This form is often reasonable when the bases represent independent, small interfaces. It becomes more complicated when bases contain state or share a common ancestor. The diamond problem illustrates the issue:

```cpp

class Device { protected: int id{}; };

class Printer : public Device {};

class Scanner : public Device {};

class Copier : public Printer, public Scanner {};

```

`Copier` contains two `Device` subobjects, so an unqualified reference to `id` is ambiguous. Virtual inheritance creates one shared `Device` subobject:

```cpp

class Printer : virtual public Device {};

class Scanner : virtual public Device {};

class Copier : public Printer, public Scanner {};

```

The most-derived class is responsible for constructing the virtual base. Virtual inheritance solves duplication but introduces additional conceptual and implementation complexity. It should be selected because the domain requires shared base identity, not merely to remove a compiler error.

Multiple inheritance also raises ambiguity in member lookup, conversion, and construction. Explicit qualification can resolve some ambiguities, but persistent ambiguity may signal an unclear design. Composition of independent capabilities is frequently easier to understand than a large multiple-inheritance hierarchy.

Composition Versus Inheritance

Inheritance establishes a strong, usually permanent relationship and exposes substitutability. Composition places objects inside another object and delegates work to them:

```cpp

class PaymentProcessor {

public:

virtual bool process(double amount) = 0;

virtual ~PaymentProcessor() = default;

};

class Checkout {

public:

explicit Checkout(std::unique_ptr<PaymentProcessor> p)

: processor(std::move(p)) {}

bool pay(double amount) { return processor->process(amount); }

private:

std::unique_ptr<PaymentProcessor> processor;

};

```

`Checkout` is not a kind of payment processor; it uses one. Composition supports replaceable policies, dependency injection, localized responsibilities, and changing relationships at runtime. It avoids exposing inherited operations that do not make sense for the containing class.

Inheritance remains appropriate when a stable abstraction is central to the domain, clients genuinely need substitutability, and the base contract is designed for extension. Composition is generally preferable when the desired relationship is “has-a,” “uses-a,” or “implemented-with.” The decision should consider ownership, lifecycle, extensibility, testing, binary compatibility, and the likelihood that the base interface will evolve.

A Practical Design Method

When designing a polymorphic hierarchy, the following reasoning sequence is useful:

1. Identify the client behaviour that must vary. Start with use cases, not class names.

2. Define the smallest coherent base contract required by those clients.

3. State invariants, ownership, error behaviour, and lifetime expectations.

4. Test candidate subtypes against substitutability scenarios.

5. Make dynamic operations virtual and mark implementations `override`.

6. Provide a virtual destructor if deletion through the base is possible.

7. Store polymorphic objects through references, pointers, or smart pointers—not base values.

8. Prefer composition for optional or replaceable implementation details.

9. Use multiple inheritance mainly for independent interfaces; introduce virtual bases only when shared identity is required.

10. Test both common behaviour and boundary cases, including destruction and failure paths.

Applied Case: Extensible Document Export Service

Background

An organisation maintains a reporting service that exports financial data as PDF, comma-separated values, and JSON. The first version contains a large conditional statement in `ExportService`, with new formats requiring edits to the same function.

Situation and Problem

The service must support third-party export formats without modifying stable business logic. It must also be testable, safely owned, and capable of reporting export failure. A careless hierarchy could expose format-specific functions or permit deletion through an unsafe base pointer.

Analysis

The varying behaviour is document serialization, not the identity of the service itself. An abstract exporter is a suitable boundary. Each exporter must honour a common contract: it accepts a report, either returns a valid serialized representation or reports failure according to the agreed policy, and does not retain references beyond the operation unless documented.

A suitable design is:

```cpp

struct ReportData {

std::string title;

std::vector<std::pair<std::string, double>> values;

};

class Exporter {

public:

virtual std::string exportReport(const ReportData&) const = 0;

virtual ~Exporter() = default;

};

class JsonExporter final : public Exporter {

public:

std::string exportReport(const ReportData& r) const override {

// Actual production code must perform proper JSON escaping.

return "{\"title\":\"" + r.title + "\"}";

}

};

class ExportService {

public:

explicit ExportService(std::unique_ptr<Exporter> e)

: exporter(std::move(e)) {}

std::string run(const ReportData& report) const {

return exporter->exportReport(report);

}

private:

std::unique_ptr<Exporter> exporter;

};

```

Approach and Outcome

The service depends on the abstraction, while concrete exporters can be supplied through dependency injection. A test exporter can record calls or return controlled failures. `std::unique_ptr` expresses exclusive ownership, and the virtual destructor makes destruction safe. `final` documents that `JsonExporter` is not intended as an extension point. The design also permits runtime selection of an exporter without a growing conditional statement.

In production, the contract would need explicit encoding rules, locale and numeric-format requirements, security controls against injection, bounded memory behaviour, and a defined error mechanism. The example illustrates the architecture rather than a complete serialization library.

Learning Points

The case demonstrates that runtime polymorphism is valuable when a stable client needs interchangeable implementations. It also shows that the abstraction should represent the varying responsibility narrowly. If exporters require unrelated configuration, authorization, or transport operations, those concerns should be composed rather than placed in the exporter base class.

Challenges, Risks, and Common Mistakes

A fragile base class may break derived classes when a new virtual function, changed invariant, or altered call sequence is introduced. Document extension points and keep interfaces small. Calling virtual functions from constructors or destructors does not dispatch to the most-derived override; the derived portion is not yet constructed or has already been destroyed. Use non-virtual initialization helpers instead.

Downcasting with `static_cast` or repeated `dynamic_cast` often indicates that the base abstraction is missing an operation or that inheritance is being used for code reuse alone. `dynamic_cast` has legitimate uses, especially when safely querying a polymorphic hierarchy, but it should not replace a coherent interface.

Default arguments on virtual functions are statically bound. If a base virtual function has a default argument and a derived override has another, a call through a base reference uses the base default while executing the derived body. Avoid differing defaults in an overriding hierarchy.

Another mistake is assuming that a virtual function is automatically invoked in every context. Qualified calls, constructors, destructors, and calls made through a non-polymorphic value have different semantics. Finally, public inheritance should not be selected merely because the derived class wants to reuse implementation. Private helper objects or composition often provide safer reuse.

Professional and Ethical Considerations

Polymorphic interfaces influence reliability, security, and governance. An extension mechanism that loads third-party implementations must define trust boundaries, version compatibility, resource limits, and failure isolation. Deserializers and exporters should validate input and output, escape data correctly, and prevent injection or uncontrolled resource consumption.

In regulated or safety-critical systems, substitutability claims should be supported by tests, interface specifications, traceability, and review. A derived implementation that silently weakens authorization, logging, retention, or error reporting can create compliance and operational risks even if it compiles correctly. Clear ownership and deterministic destruction are also important when objects manage files, locks, credentials, or network resources.

Contemporary C++ Developments and Best Practices

Modern C++ favours RAII, smart pointers, `override`, `final`, `noexcept` specifications where justified, and standard library abstractions. Concepts and templates can express compile-time polymorphism when the set of supported operations is known and runtime interchangeability is unnecessary. Compile-time polymorphism can remove virtual dispatch and support highly optimized code, while runtime polymorphism is often more suitable for plugin boundaries, configuration-driven selection, and heterogeneous collections.

Type erasure, as used conceptually by facilities such as `std::function`, offers another approach: it stores objects with a required callable interface without requiring them to share a visible inheritance hierarchy. The choice among inheritance, templates, and type erasure depends on compilation boundaries, performance, ownership, binary compatibility, and the need for open-ended extension.

Best practice is therefore not “use virtual functions everywhere” or “avoid inheritance everywhere.” Instead, make the variation explicit, define a narrow contract, protect invariants, use safe lifetime management, and measure performance and maintainability in the target system.

Chapter Summary

Inheritance allows a derived class to extend a base-class subobject, but public inheritance should express genuine behavioural substitutability. Virtual functions enable dynamic binding: the interface is checked through the declared type while the override is selected from the actual object type. Abstract classes and pure virtual functions define contracts for interchangeable implementations, and `override` protects against signature errors.

Polymorphic objects should be accessed through references or pointers rather than copied by value, because base-value copying causes object slicing. A polymorphic base normally requires a virtual destructor so that derived destruction through a base pointer is defined. Multiple inheritance is useful especially for independent interfaces; virtual inheritance resolves shared-base duplication but adds complexity. Composition is often preferable when a class uses or delegates to another object rather than being a true subtype. Sound designs evaluate invariants, ownership, failure behaviour, testing, security, and long-term evolution—not merely syntax.

Key Terms

Inheritance: Mechanism by which a derived class acquires and extends a base-class interface or implementation.

Base class: A class whose members and contract may be inherited by another class.

Derived class: A class that extends or specializes a base class.

Public inheritance: Inheritance that preserves public substitutability between derived and base types.

Virtual function: A member function whose call can be dynamically dispatched.

Dynamic binding: Runtime association of a virtual call with the override for the actual object type.

Static binding: Compile-time association of a call with a function based on the expression’s declared type.

Overriding: Providing a compatible derived implementation of an inherited virtual function.

Abstract class: A class that cannot be instantiated because it has at least one pure virtual function.

Pure virtual function: A virtual function declared with `= 0`, requiring concrete derived classes to implement it.

Substitutability: The property that a derived object can safely be used wherever its base type is expected.

Object slicing: Loss of the derived portion when a derived object is copied into a base object by value.

Virtual destructor: A destructor enabling correct destruction through a base pointer or reference-based ownership interface.

Multiple inheritance: Derivation from more than one direct base class.

Virtual inheritance: An inheritance form that provides one shared virtual-base subobject in a diamond hierarchy.

Composition: Building a class from contained objects and delegating responsibilities to them.

Downcasting: Converting a base-class handle to a more specific derived-class type.

Type erasure: Hiding an object’s concrete type while preserving access to a required operation set.

Dependency injection: Supplying a component’s dependencies from outside rather than constructing them internally.

Review Questions

Short Answer Questions

1. What distinction exists between overriding and overloading in C++?

2. Why should a polymorphic base class usually have a virtual destructor?

3. What is object slicing, and how can it be avoided?

4. What role does the `override` specifier play?

5. Why is public inheritance associated with substitutability?

Long Answer Questions

1. Explain static and dynamic binding with suitable C++ examples.

2. Describe the design and use of abstract classes and pure virtual functions.

3. Discuss the Liskov Substitution Principle and analyse the rectangle–square example.

4. Explain multiple inheritance, the diamond problem, and virtual inheritance.

5. Compare inheritance and composition for implementing replaceable system behaviour.

Analytical / Application Questions

1. A collection stores `std::vector<Base>` objects, but several concrete derived objects are inserted into it. Analyse the likely defect and propose a redesign.

2. A base pointer deletes a derived network connection whose destructor is non-virtual. Explain the risk and specify a safe ownership design.

3. A derived class requires clients to test its runtime type before calling a special operation. Evaluate whether the hierarchy or its base contract should be redesigned, and justify your answer.

22. Multiple Choice Questions with Answers

Question 1

A call is made through a pointer of type Base* to an object whose dynamic type is Derived. Which condition is necessary for the Derived implementation to be selected?

A. The function must be static

B. The function must be virtual and correctly overridden

C. The base class must have private data

D. The object must be copied by value

Correct Answer: B. The function must be virtual and correctly overridden

Question 2

What happens when a derived object is initialized as a base object by value?

A. The base object gains dynamic type information

B. The derived portion is sliced away

C. The operation is always ill-formed

D. The derived destructor is called twice

Correct Answer: B. The derived portion is sliced away

Question 3

Which declaration best supports deletion through a Base pointer?

A. Base() = default;

B. static ~Base() = default;

C. virtual ~Base() = default;

D. friend ~Base() = default;

Correct Answer: C. virtual ~Base() = default;

Question 4

What does the override specifier enable the compiler to verify?

A. That a function is overloaded

B. That a function overrides a virtual base function

C. That a class has no data members

D. That a function is evaluated at compile time

Correct Answer: B. That a function overrides a virtual base function

Question 5

Which statement best describes an abstract class?

A. It contains only private members

B. It cannot contain constructors

C. It has at least one pure virtual function and cannot be instantiated directly

D. It cannot be used through pointers

Correct Answer: C. It has at least one pure virtual function and cannot be instantiated directly

Question 6

A class inherits from two paths that each contain a Device base subobject. Which feature can provide one shared Device subobject?

A. Function overloading

B. Virtual inheritance

C. Object slicing

D. Static binding

Correct Answer: B. Virtual inheritance

Question 7

Which situation most strongly favours composition over public inheritance?

A. A derived object must be safely usable as the base type

B. A class merely uses a replaceable service and is not a kind of that service

C. Several classes share a stable behavioural contract

D. A base interface defines required virtual operations

Correct Answer: B. A class merely uses a replaceable service and is not a kind of that service

Question 8

Why can calling a virtual function from a constructor be dangerous?

A. The object cannot contain a vtable

B. The most-derived portion has not yet been constructed

C. Constructors are always private

D. Virtual functions cannot return values

Correct Answer: B. The most-derived portion has not yet been constructed

Question 9

Which design most appropriately represents exclusive ownership of a polymorphic exporter?

A. std::unique_ptr<Exporter>

B. Exporter by value in a raw integer

C. std::vector<Exporter> for heterogeneous objects

D. A non-owning reference stored after the exporter is destroyed

Correct Answer: A. std::unique_ptr<Exporter>

Question 10

Repeated downcasts from a base pointer to discover which operation to perform most often suggest that:

A. The base abstraction may be incomplete or the hierarchy may be inappropriate

B. The destructor must be non-virtual

C. Object slicing is required

D. Multiple inheritance is always necessary

Correct Answer: A. The base abstraction may be incomplete or the hierarchy may be inappropriate

Object Oriented Programming using 'C++
Chapter 6 of 15