Chapter 12: Inheritance and Polymorphism
Inheritance lets one class reuse and extend another; polymorphism lets code treat related objects through a shared interface. Used together, they enable substitution, late binding, and clean separation of interface from implementation. This chapter explains the forms of inheritance in C++, how dynamic dispatch works, where abstract classes fit, how to reason about multiple inheritance and the diamond, what object slicing is, and when composition is the better choice.
Types of inheritance
The access specifier in a base clause controls how the base’s public and protected interface is viewed from the outside through the derived type. It does not change the actual accessibility inside the derived class; it changes the relationship that clients see when they use the derived type.
The inheritance specifier
With public inheritance, a base’s public stays public and protected stays protected when reached through the derived type. With protected inheritance, both public and protected base members become protected as seen through the derived type. With private inheritance, both become private through the derived type. Inside the derived class body, the usual access rules still apply.
struct Base {
void pub();
protected:
void prot();
};
struct D1 : public Base { }; // Base::pub() is public via D1; Base::prot() is protected via D1
struct D2 : protected Base { }; // both are protected via D2
struct D3 : private Base { }; // both are private via D3
Use public inheritance for true “is a” relationships
Choose public inheritance when the derived type is substitutable for the base. If clients can use a Base reference and safely pass a Derived, substitution holds. protected and private inheritance express implementation reuse; they do not model “is a”; prefer composition for that kind of reuse in modern C++.
Base object should remain valid for a Derived object used through a Base&.
virtual functions and dynamic dispatch
A member function declared with virtual participates in dynamic dispatch. Calls made through a base pointer or reference are resolved at run time to the most derived override that matches the dynamic type of the object. This enables polymorphic behavior with a stable base interface.
Declaring, overriding, and finalizing
Mark a base function virtual and override it in derived classes. Use override on the derived declaration to request compile time checking; use final to prevent further overrides. Virtual dispatch requires at least one virtual function in the class to create a vtable on common ABIs.
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle : Shape {
double r{};
explicit Circle(double rr) : r(rr) {}
double area() const override { return 3.141592653589793 * r * r; }
};
double total_area(const std::vector<std::unique_ptr<Shape>>& v) {
double sum = 0;
for (auto& s : v) sum += s->area(); // dynamic dispatch
return sum;
}
Call rules and qualified lookup
Virtual dispatch applies only to calls made through a base class subobject and only for functions declared virtual. Constructors do not dispatch virtually; destructors dispatch virtually when invoked through a base pointer if the base destructor is virtual. A qualified call like Base::f() suppresses virtual dispatch and calls the named function.
Abstract classes and interfaces
An abstract class has one or more pure virtual functions. Such a class cannot be instantiated, but it defines an interface for derived types to implement. In C++ there is no special “interface” keyword; a class with only pure virtuals, a virtual destructor, and no data members often serves the role.
Declaring pure virtual functions
Use = 0 on a virtual declaration to make it pure. Derived classes must override all inherited pure virtuals to become concrete. A pure virtual can still provide a definition that derived overrides may call.
struct Stream {
virtual ~Stream() = default;
virtual std::size_t read(std::byte* buf, std::size_t n) = 0;
virtual std::size_t write(const std::byte* buf, std::size_t n) = 0;
};
struct FileStream : Stream {
std::FILE* f{};
explicit FileStream(const char* path, const char* mode) : f(std::fopen(path, mode)) {}
std::size_t read(std::byte* buf, std::size_t n) override { return std::fread(buf, 1, n, f); }
std::size_t write(const std::byte* buf, std::size_t n) override { return std::fwrite(buf, 1, n, f); }
};
Separating interface and implementation
Keep the abstract base minimal and stable; push optional behavior into free functions or nonvirtual helpers. This reduces coupling and keeps your polymorphic surface small. Prefer nonvirtual template helpers when behavior does not need run time polymorphism.
Multiple inheritance and the diamond
A class may inherit from more than one base. This can model types that must satisfy several independent interfaces. It can also create the diamond shape when two bases share a common base. Virtual base classes resolve the duplication at the cost of added complexity.
Combining independent interfaces
Use multiple inheritance safely when the bases are pure interface classes. Since such bases typically carry no data, there is no ambiguity about member layout, and the derived class supplies one concrete implementation that satisfies all required functions.
struct Drawable { virtual ~Drawable() = default; virtual void draw() const = 0; };
struct Updatable { virtual ~Updatable() = default; virtual void update(double dt) = 0; };
struct Sprite : Drawable, Updatable {
void draw() const override { /* … */ }
void update(double dt) override { /* … */ }
};
The diamond and virtual base classes
When two bases share a common base, the most derived class normally contains two copies of that common base. Mark the shared base as a virtual base to ensure there is exactly one shared subobject. Construction of a virtual base is the responsibility of the most derived class.
struct A { int id; };
struct B : virtual A { };
struct C : virtual A { };
struct D : B, C {
D() { id = 42; } // only one A subobject exists
};
virtual inheritance, document who constructs it and which invariants it owns.
Object slicing and non virtual pitfalls
Assigning a derived object to a base object by value slices away the derived part, leaving only the base subobject. Likewise, calling a nonvirtual function through a base reference binds statically and cannot reach derived behavior. These problems appear subtle in simple code and cause surprises at scale.
Using references or smart pointers
Store polymorphic objects via references or smart pointers rather than by value. Prefer std::unique_ptr<Base> for ownership. If sharing is required, use std::shared_ptr<Base> with care and clear lifetime rules.
struct Base { virtual ~Base() = default; virtual int kind() const = 0; };
struct Derived : Base { int kind() const override { return 2; } };
Derived d;
Base b = d; // slicing; b no longer knows about Derived
Base& rb = d; // no slicing
std::unique_ptr<Base> p = std::make_unique<Derived>(); // no slicing
Mark interfaces virtual and overrides override
If dynamic behavior is intended, the base must declare the function virtual and each derived implementation should use override. This catches signature mismatches and prevents accidental static binding that defeats polymorphism.
virtual on the base or a mismatched signature in the derived silently turns a virtual call into a nonvirtual call; the program still compiles and runs with the wrong behavior.
When to prefer composition
Prefer composition when you need to assemble behavior from parts rather than model an is a relationship. Composition keeps dependencies explicit, avoids the inheritance hierarchy trap, supports runtime swapping of strategies, and plays well with value semantics and templates.
Modeling roles with strategies
Use small interfaces and hold them by value or smart pointer inside a class. Switching strategies at run time often solves the same problem that inheritance tries to solve, while keeping object graphs flatter and more testable.
struct Logger {
virtual ~Logger() = default;
virtual void write(std::string_view msg) = 0;
};
struct ConsoleLogger : Logger {
void write(std::string_view msg) override { std::fputs(msg.data(), stdout); std::fputc('\n', stdout); }
};
class Service {
std::unique_ptr<Logger> log_;
public:
explicit Service(std::unique_ptr<Logger> lg) : log_(std::move(lg)) {}
void do_work() {
log_->write("starting");
/* … */
log_->write("done");
}
};
Choosing composition over inheritance
If you are not expressing true substitutability, choose composition. If the base would carry mutable state shared across unrelated features, choose composition. If you want to vary behavior per instance and not per type, choose composition. If testing becomes easier with injected roles, choose composition. In short, reach for inheritance last.
public inheritance only when substitutability is clear and proven by use cases.