Phase 2 · Classes, Ownership & Resource SafetyModule 13~54 min read

Inheritance, Polymorphism & Interfaces

Use runtime polymorphism safely, prevent slicing, design abstract interfaces, and know when composition is the stronger choice.

What you'll learn

Runtime polymorphism lets code work through a stable base contract while concrete types choose behavior. You will build an abstract interface, preserve object identity with references and smart pointers, prevent slicing, and compare inheritance with composition and variants.

By the end, you'll be able to:

  • Define public base relationships and override virtual behavior
  • Build abstract interfaces with safe virtual destructors
  • Recognize object slicing and manage polymorphic ownership
  • Choose composition, templates, or variants when inheritance is not the best fit

Base and derived types

Public inheritance states that a derived object can be used wherever the base contract is expected—an “is-a” relationship. The derived object contains a base subobject and may add state or behavior. Protected inheritance and private inheritance model different reuse relationships and are far less common in application interfaces.

shapes.cpp
#include <cmath>
#include <iostream>

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

class Circle final : public Shape {
public:
    explicit Circle(double radius) : radius_{radius} {}
    double area() const override { return std::numbers::pi * radius_ * radius_; }
private:
    double radius_{};
};
A complete file would include <numbers> for std::numbers::pi.

Key idea

Public inheritance is a substitutability promise, not simply a way to reuse implementation. Every derived object must honor the base type's documented contracts.

Virtual dispatch

A virtual call made through a base reference or pointer selects the final overrider for the object's dynamic type. Add override to every intended override so a signature mismatch becomes a compiler error. final can prevent further overriding or derivation.

dispatch.cpp
#include <iostream>
#include <string_view>

class Formatter {
public:
    virtual void write(std::string_view text) const = 0;
    virtual ~Formatter() = default;
};

class ConsoleFormatter final : public Formatter {
public:
    void write(std::string_view text) const override {
        std::cout << "[info] " << text << '\n';
    }
};

void report(const Formatter& formatter) { formatter.write("ready"); }

int main() {
    ConsoleFormatter formatter;
    report(formatter);
}

Abstract interfaces

A pure virtual function ends with = 0, making the class abstract. Interface classes normally contain a small coherent set of pure virtual operations plus a virtual destructor. They should state ownership, failure, mutation, and lifetime rules just as carefully as non-virtual APIs.

Keyword/formProtection
virtualEnables dynamic dispatch
= 0Requires a concrete final overrider
overrideChecks that a function actually overrides
finalStops further override or derivation
virtual ~Base()Makes destruction through Base* safe

Polymorphic bases need a destruction policy

If callers can delete a derived object through a base pointer, the base destructor must be public and virtual. Some specialized designs instead use a protected non-virtual destructor to forbid that deletion.

Object slicing

Copying a derived object into a base object keeps only the base subobject. Dynamic type and derived state are sliced away, so later virtual calls cannot recover them. Pass polymorphic values by reference or pointer and store owning polymorphic objects through smart pointers.

polymorphic_owners.cpp
#include <iostream>
#include <memory>
#include <vector>

class Command {
public:
    virtual void run() const = 0;
    virtual ~Command() = default;
};

class Save final : public Command {
public:
    void run() const override { std::cout << "save\n"; }
};

int main() {
    std::vector<std::unique_ptr<Command>> commands;
    commands.push_back(std::make_unique<Save>());
    for (const auto& command : commands) command->run();
}

Tip

Use std::unique_ptr<Base> by default for polymorphic ownership. Shared ownership is a separate lifetime decision, not a requirement of polymorphism.

Composition over inheritance

Composition stores a collaborator as a member and forwards the work it needs. It avoids exposing a base relationship, allows behavior to be replaced independently, and keeps dependencies focused. Inheritance is strongest when substitutability and runtime extension are genuinely required, not merely to avoid duplicating a few lines.

composition.cpp
#include <string_view>

class Logger {
public:
    virtual void log(std::string_view) = 0;
    virtual ~Logger() = default;
};

class ImportService {
public:
    explicit ImportService(Logger& logger) : logger_{logger} {}
    void run() {
        logger_.log("import started");
        // perform the import
    }
private:
    Logger& logger_; // borrows a collaborator
};

Templates and variants

Templates provide compile-time polymorphism when concrete types are known during compilation and performance or inlining matters. std::variant represents a closed set of alternatives with value semantics and exhaustive visitation. Virtual interfaces represent an open set of runtime-defined implementations.

TechniqueSet of typesDispatchOwnership style
Virtual interfaceOpenRuntimeUsually references or smart pointers
Template/conceptOpen at compilationCompile timeOften values or references
std::variantClosedRuntime visitationValue
CompositionCollaborator-definedEitherMember, reference, or owner

Note

Multiple inheritance is most manageable when combining small interface-only bases. Multiple stateful base implementations can create ambiguity and complex construction relationships.

Recap & quick check

Key takeaways

  • Public inheritance promises substitutability under the base contract.
  • Virtual calls dispatch through base references or pointers; override catches signature mistakes.
  • A polymorphic base needs an intentional destruction policy, commonly a virtual destructor.
  • Passing or storing polymorphic objects by value causes slicing.
  • Composition, templates, and variants are often clearer alternatives to inheritance.

Quick check

1. What does public inheritance promise?

2. Why write override?

3. What is object slicing?

4. Which technique naturally models a closed set of value alternatives?

Phase 2 complete. Next, Module 14 — Templates & Concepts begins generic programming with compile-time contracts.