C++ Composition and Runtime Polymorphism
Assemble behavior through composition, use virtual interfaces only at real variation points, and prevent slicing and ownership mistakes.
Before this lesson
Distinguish has-a and is-a relationships
Define and use a small abstract interface
Avoid slicing and non-virtual destruction
The short answer
Prefer composition when one object uses another. Use runtime polymorphism when callers need one interface for several implementations, give polymorphic bases a virtual destructor, and pass them by reference or smart pointer.
Composition keeps roles independent
Composition models a has-a or uses-a relationship. A reminder service uses a message sink; it is not a kind of message sink. Supplying that collaborator through the constructor or function makes the dependency visible and replaceable.
Many designs need only concrete composition. Introduce an interface when multiple providers, isolated testing, or a stable boundary creates real variation. An interface for every class adds indirection without flexibility.
#include <iostream>
#include <string_view>
class ConsoleSink
{
public:
void send(std::string_view message) const
{
std::cout << message << '\n';
}
};
class ReminderService
{
public:
explicit ReminderService(const ConsoleSink& sink) : sink_{sink} {}
void remind() const { sink_.send("Practice one example"); }
private:
const ConsoleSink& sink_;
};
int main() { ConsoleSink sink; ReminderService service{sink}; service.remind(); }Virtual functions enable runtime dispatch
A pure virtual function declares a contract that derived classes must implement. Calling through a base reference or pointer selects the derived override at runtime. Add override so the compiler verifies that a derived declaration actually matches a virtual base operation.
Keep interfaces small and caller-oriented. A sink needs send; it does not need every configuration operation of each concrete delivery system.
| Tool | Binding | Useful when |
|---|---|---|
| Concrete composition | Direct | One stable collaborator |
| Virtual interface | Runtime | Provider chosen at runtime |
| Function template | Compile time | Same operation across types without shared base |
| std::function | Runtime callable value | Store callbacks with one signature |
Polymorphic objects need safe lifetime
Pass polymorphic objects by reference or pointer to preserve dynamic type. If ownership transfers, std::unique_ptr<Base> commonly owns one derived object. A virtual base destructor ensures destruction reaches the derived resources.
Non-owning references must not outlive the implementation. Constructor injection by reference works when the composition root owns providers longer than their consumers. Use unique ownership when that lifetime relationship needs to be stored and transferred.
Inheritance is a substitutability promise
Public inheritance says a derived object can safely stand wherever the base is expected without breaking the base contract. Reusing a few lines of implementation is not enough. Deep hierarchies spread behavior across files and make construction and ownership harder to see.
Prefer composition for interchangeable roles and helpers. Use inheritance when the abstraction is stable, substitution is meaningful, and runtime dispatch is truly required.
Quick knowledge check
Answer before you reveal.
01Why should a polymorphic base destructor be virtual?
Deleting a derived object through a base pointer must invoke the complete derived destruction sequence.
02What is object slicing?
Copying a derived object into a base object by value keeps only the base subobject and loses derived state and dynamic behavior.
Exercise
Practice challenge
Define a MessageSink interface with a virtual send function, implement a ConsoleSink, and inject it by reference into a ReminderService.
Requirements
- ReminderService depends on the interface rather than ConsoleSink
- The base destructor is virtual
- No owning raw pointer or global service is introduced
Optional extension: Add a RecordingSink that stores messages for a test.
Open in C++ compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Is every interface an abstract base class?
In conventional C++, a class with pure virtual operations often represents a runtime interface, though templates can provide compile-time polymorphism.
Should every class be final?
Use final when extension is not part of the contract, but focus first on whether inheritance is needed at all.
What is dependency injection here?
The caller supplies a collaborator, commonly through a constructor or function, instead of the consumer constructing a fixed implementation.