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.
#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_{};
};Key idea
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.
#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/form | Protection |
|---|---|
virtual | Enables dynamic dispatch |
= 0 | Requires a concrete final overrider |
override | Checks that a function actually overrides |
final | Stops further override or derivation |
virtual ~Base() | Makes destruction through Base* safe |
Polymorphic bases need a destruction policy
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.
#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
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.
#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.
| Technique | Set of types | Dispatch | Ownership style |
|---|---|---|---|
| Virtual interface | Open | Runtime | Usually references or smart pointers |
| Template/concept | Open at compilation | Compile time | Often values or references |
| std::variant | Closed | Runtime visitation | Value |
| Composition | Collaborator-defined | Either | Member, reference, or owner |
Note
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.