SimplifyC++ Article
Composition in Modern C++: From Inheritance Trees to Component-Based Architecture

One of the most influential principles that has become deeply established in object-oriented software engineering is:
Favor Composition over Inheritance
In other words:
Prefer assembling components over inheritance when composition provides the more natural and simpler expression of the design.
However, this statement is often misunderstood as if Composition were a recent technique introduced to replace Inheritance, or as if inheritance itself had become an outdated mechanism that should be abandoned.
That is not correct.
Composition is an old concept in software engineering, and it gained enormous visibility with the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software, which popularized the famous principle:
Favor object composition over class inheritance.
The purpose was never to eliminate inheritance. It was to prevent inheritance from being used where it does not naturally belong, particularly when the only objective is to reuse implementation.
Modern C++ makes Composition even more important because the language now provides a rich set of mechanisms for building systems from independent components:
- RAII
- Value Semantics
- Templates
- Concepts
- Lambdas
- Smart Pointers
std::optionalstd::variant- Compile-Time Polymorphism
- Runtime Polymorphism
- Policy-Based Design
Composition in modern C++ is therefore much more than simply placing one object inside another class.
It has become an architectural philosophy for building systems from small capabilities that can be combined, replaced, and adapted according to the needs of the design.
What Is Composition?
Let us begin with the simplest possible example.
We have an engine:
class Engine {
public:
void start() {
// Start the engine
}
};
And we have a car:
class Car {
private:
Engine engine_;
public:
void start() {
engine_.start();
}
};
The relationship here is:
Car HAS-A Engine
The car has an engine.
That is Composition.
If instead we wrote:
class Car : public Engine {
};
we would be expressing a completely different relationship:
Car IS-A Engine
That would mean that a car is a kind of engine, which is obviously incorrect from a design perspective.
The basic distinction can therefore be summarized as:
Inheritance → IS-A
Composition → HAS-A
USES-A
CONSISTS-OF
Composition Is More Than a Data Member
The simplest form of Composition looks like this:
class A {
private:
B b_;
};
But reducing Composition to this syntax alone misses much of its architectural significance.
The broader engineering idea is:
Instead of building behavior through increasingly deep inheritance layers, build the system from relatively independent components and assemble those components to produce the required behavior.
In modern C++, we can therefore think of several forms of composition:
Value Composition
Resource Composition
Runtime Composition
Compile-Time Composition
Generic Composition
Policy Composition
Behavior Composition
Data Composition
The Problem Is Not Inheritance Itself
Inheritance is a fundamental and valuable OOP mechanism.
The problem appears when it is used to represent a relationship that is not genuinely an IS-A relationship.
Suppose we want to add logging functionality to a database:
class LoggerBase {
protected:
void log(std::string_view message) {
// Write message
}
};
class Database : public LoggerBase {
public:
void connect() {
log("Connecting...");
}
};
The code works.
But the design says:
Database IS-A LoggerBase
That is not true.
A database is not a logger.
It merely uses a logger.
A more accurate design is:
class Logger {
public:
void log(std::string_view message) {
// Write message
}
};
class Database {
private:
Logger logger_;
public:
void connect() {
logger_.log("Connecting...");
}
};
Now the relationship becomes:
Database HAS-A Logger
This models reality much more accurately.
Code Reuse Alone Is Not a Good Reason for Inheritance
A classic mistake looks like this:
class Utilities {
protected:
void load_config();
void write_log();
void validate();
};
class Application : public Utilities {
};
Why does Application inherit from Utilities?
Only because it wants to reuse several functions.
But:
Application IS-A Utilities
has no meaningful semantic interpretation.
Composition is more appropriate:
class Application {
private:
Utilities utilities_;
public:
void run() {
utilities_.load_config();
}
};
And if those utility operations do not maintain any state at all, free functions may be even clearer:
void load_config();
void write_log();
void validate();
This leads to an important principle in modern C++:
Do not use a class when a function is sufficient, and do not use inheritance when composition is sufficient.
Why Can Inheritance Trees Become a Problem?
A hierarchy may begin innocently:
Base
├── A
├── B
└── C
Then it grows:
Base
├── A
│ ├── A1
│ ├── A2
│ └── A3
│
├── B
│ ├── B1
│ └── B2
│
└── C
├── C1
└── C2
Eventually, derived classes may start depending on:
protecteddata- virtual hooks
- implementation details inside the base class
- specific call ordering
- hidden assumptions between base and derived classes
At that point, changing the base class can become dangerous because many derived classes implicitly depend on its behavior.
White-Box Reuse vs. Black-Box Reuse
Inheritance is sometimes described as:
White-Box Reuse
because the derived class may know internal details about the base class.
class Base {
protected:
int state_{};
void update_state() {
++state_;
}
};
class Derived : public Base {
public:
void process() {
state_ = 100;
update_state();
}
};
Here, Derived clearly depends on the internal representation and behavior of Base.
Composition moves us closer to:
Black-Box Reuse
class Compressor {
public:
void compress(std::span<const std::byte> data) {
// Compression implementation
}
};
class ArchiveWriter {
private:
Compressor compressor_;
public:
void write(std::span<const std::byte> data) {
compressor_.compress(data);
}
};
ArchiveWriter does not need to know how compression works internally.
All it needs is the contract:
compressor_.compress(data);
This reduces coupling between components.
The Combinatorial Explosion Problem
Suppose we begin with a storage system:
class Storage {
public:
virtual void save() = 0;
virtual ~Storage() = default;
};
Then:
class FastStorage : public Storage {
public:
void save() override {
// Fast storage
}
};
Now we add compression:
Storage
FastStorage
CompressedStorage
FastCompressedStorage
Then encryption:
EncryptedStorage
EncryptedCompressedStorage
FastEncryptedStorage
FastEncryptedCompressedStorage
Then networking:
NetworkStorage
EncryptedNetworkStorage
CompressedNetworkStorage
FastEncryptedCompressedNetworkStorage
...
Each new capability starts multiplying the number of required concrete types.
This is a form of:
Combinatorial Explosion
Composition approaches the problem differently:
Make each capability an independent component instead of making every possible combination a new derived class.
Static Composition with Templates
We can define independent compression policies:
struct FastCompression {
void compress() {
// Fast compression
}
};
struct MaximumCompression {
void compress() {
// Maximum compression
}
};
Then compose them into storage:
template<typename Compression>
class Storage {
private:
Compression compression_;
public:
void save() {
compression_.compress();
// Save the data
}
};
Usage:
Storage<FastCompression> fast_storage;
Storage<MaximumCompression> maximum_storage;
We did not need:
Base class
Virtual function
VTable
Runtime dispatch
The behavior was composed during compilation.
This is:
Compile-Time Composition
Concepts Make the Contract Explicit
With C++20, we can directly describe the capability expected from a component:
template<typename T>
concept CompressionPolicy =
requires(T compressor) {
compressor.compress();
};
Then:
template<CompressionPolicy Compression>
class Storage {
private:
Compression compression_;
public:
void save() {
compression_.compress();
}
};
This represents an important change in design thinking.
Traditional OOP may ask:
What class do you inherit from?
Generic programming asks:
What can your type do?
The focus moves from ancestry to capability.
Static Polymorphism
Traditional runtime polymorphism looks like this:
class Compressor {
public:
virtual void compress() = 0;
virtual ~Compressor() = default;
};
Then:
class ZipCompressor final : public Compressor {
public:
void compress() override {
// ZIP implementation
}
};
Compile-time polymorphism can instead be written as:
template<typename Compressor>
class Storage {
private:
Compressor compressor_;
public:
void save() {
compressor_.compress();
}
};
There is no need for:
virtual- a VTable
- dynamic dispatch
But this does not mean templates are always better.
They may introduce costs such as:
- more template instantiations
- longer compilation times
- larger binary size in some cases
- the requirement that relevant types be known at compile time
So the real principle is:
Choose the form of polymorphism that matches the actual flexibility required by the system.
Runtime Composition
What if the compressor is chosen while the application is running?
For example:
compression = zstd
or:
compression = lz4
Runtime polymorphism becomes entirely reasonable:
class Compressor {
public:
virtual void compress() = 0;
virtual ~Compressor() = default;
};
Then:
class ZstdCompressor final : public Compressor {
public:
void compress() override {
// Zstd
}
};
class Lz4Compressor final : public Compressor {
public:
void compress() override {
// LZ4
}
};
But Storage does not inherit from Compressor.
It owns one:
class Storage {
private:
std::unique_ptr<Compressor> compressor_;
public:
explicit Storage(std::unique_ptr<Compressor> compressor)
: compressor_{std::move(compressor)} {
}
void save() {
compressor_->compress();
}
};
Usage:
auto compressor =
std::make_unique<ZstdCompressor>();
Storage storage{
std::move(compressor)
};
The design becomes:
Compressor
/ \
Zstd LZ4
\ /
\ /
Inheritance
│
▼
Interface
│
HAS-A
│
▼
Storage
This demonstrates an important principle:
Composition and Inheritance are not enemies.
Inheritance can be used inside a small abstraction, while Composition is used to assemble the larger system.
A Renderer Example
We can define a renderer interface:
class Renderer {
public:
virtual void render() = 0;
virtual ~Renderer() = default;
};
A Vulkan implementation:
class VulkanRenderer final : public Renderer {
public:
void render() override {
// Vulkan rendering
}
};
And an OpenGL implementation:
class OpenGLRenderer final : public Renderer {
public:
void render() override {
// OpenGL rendering
}
};
The application then composes a renderer:
class Application {
private:
std::unique_ptr<Renderer> renderer_;
public:
explicit Application(std::unique_ptr<Renderer> renderer)
: renderer_{std::move(renderer)} {
}
void draw() {
renderer_->render();
}
};
Here, inheritance provides a real polymorphic abstraction.
Composition builds the application around that abstraction.
Renderer
/ \
Vulkan OpenGL
\ /
Polymorphism
│
▼
Application
HAS-A Renderer
Composition and RAII
This is where C++ becomes particularly powerful.
class Server {
private:
Logger logger_;
Socket socket_;
Database database_;
};
The lifetime of the components is naturally tied to the lifetime of the server:
Server
│
├── Logger
├── Socket
└── Database
When Server is created, its members are created.
When it is destroyed, its members are destroyed automatically.
If those types implement RAII correctly, resource management itself becomes part of the composition architecture.
For example:
class File {
public:
explicit File(std::string_view path) {
// Acquire file
}
~File() {
// Release file
}
};
Then:
class Database {
private:
File file_;
};
This is:
Resource Composition
not merely behavioral composition.
Value Composition
Not every component needs a pointer or an interface.
struct Position {
double x{};
double y{};
};
struct Velocity {
double x{};
double y{};
};
Then:
class Particle {
private:
Position position_;
Velocity velocity_;
public:
void update(double dt) {
position_.x += velocity_.x * dt;
position_.y += velocity_.y * dt;
}
};
There is no:
newshared_ptrvirtual- heap allocation
This is one of the cleanest forms of:
Value Composition
Optional Composition
Sometimes a component itself is optional:
class NetworkClient {
private:
std::optional<Proxy> proxy_;
};
The relationship becomes:
NetworkClient MAY-HAVE-A Proxy
Instead of creating separate types such as:
NetworkClient
ProxyNetworkClient
NonProxyNetworkClient
Composition with std::variant
If the set of implementations is limited and known in advance:
struct Zip {
void compress() {
// ZIP
}
};
struct Lz4 {
void compress() {
// LZ4
}
};
struct Zstd {
void compress() {
// Zstd
}
};
we can define:
using Compressor =
std::variant<Zip, Lz4, Zstd>;
Then:
class Storage {
private:
Compressor compressor_;
public:
void compress() {
std::visit(
[](auto& compressor) {
compressor.compress();
},
compressor_
);
}
};
We now have runtime selection between a closed set of compile-time-known types without requiring:
- a base class
- a virtual function
- heap allocation in many cases
Policy-Based Design
One of the most powerful forms of Composition in modern C++ is:
Policy-Based Design
We can begin with logging:
struct ConsoleLogger {
void log() {
// Console output
}
};
struct FileLogger {
void log() {
// File output
}
};
Compression:
struct FastCompression {
void compress() {
// Fast compression
}
};
struct MaximumCompression {
void compress() {
// Maximum compression
}
};
Encryption:
struct AES256 {
void encrypt() {
// AES-256 encryption
}
};
struct NoEncryption {
void encrypt() {
// No encryption
}
};
Then compose them:
template<
typename Logger,
typename Compression,
typename Encryption
>
class Storage {
private:
Logger logger_;
Compression compression_;
Encryption encryption_;
public:
void save() {
logger_.log();
compression_.compress();
encryption_.encrypt();
// Save data
}
};
A secure storage type:
using SecureStorage =
Storage<
FileLogger,
MaximumCompression,
AES256
>;
And a faster configuration:
using FastStorage =
Storage<
ConsoleLogger,
FastCompression,
NoEncryption
>;
Instead of building:
Storage
│
└── LoggedStorage
│
└── CompressedStorage
│
└── EncryptedCompressedStorage
we build:
Storage
│
┌────────┼────────┐
│ │ │
Logger Compression Encryption
Behavior is now composed rather than inherited.
A Practical Example: A Compiler
A compiler consists of several subsystems:
Compiler
│
├── Lexer
├── Parser
├── Semantic Analyzer
├── Optimizer
└── Code Generator
It can be modeled naturally:
class Compiler {
private:
Lexer lexer_;
Parser parser_;
SemanticAnalyzer semantic_;
Optimizer optimizer_;
CodeGenerator codegen_;
public:
void compile(SourceFile source) {
auto tokens = lexer_.scan(source);
auto ast = parser_.parse(tokens);
semantic_.analyze(ast);
optimizer_.optimize(ast);
codegen_.generate(ast);
}
};
There is no logical reason to write:
class Compiler : public Lexer {
};
because:
Compiler IS-A Lexer
is false.
But:
Compiler HAS-A Lexer
Compiler HAS-A Parser
Compiler HAS-A Optimizer
Compiler HAS-A CodeGenerator
accurately represents the architecture.
Composition and Single Responsibility
A server may initially look like this:
class Server {
public:
void start();
void stop();
void log();
void authenticate();
void encrypt();
void compress();
void save_to_database();
void load_from_database();
void send_packet();
void receive_packet();
};
Over time, this may become a:
God Object
Composition lets us separate responsibilities:
class Server {
private:
Logger logger_;
Authenticator authenticator_;
Encryptor encryptor_;
Compressor compressor_;
Database database_;
Network network_;
public:
void start();
void stop();
};
Each component now owns a clearer responsibility.
This helps improve:
- Separation of Concerns
- Single Responsibility
- Testability
- Replaceability
- Maintainability
Composition and Testing
Suppose we have a real logger:
struct RealLogger {
void log(std::string_view message) {
// Write real log
}
};
And another logger for testing:
struct TestLogger {
std::vector<std::string> messages;
void log(std::string_view message) {
messages.emplace_back(message);
}
};
Then:
template<typename Logger>
class Service {
private:
Logger logger_;
public:
explicit Service(Logger logger)
: logger_{std::move(logger)} {
}
void execute() {
logger_.log("Executing");
}
};
Production:
Service service{
RealLogger{}
};
Testing:
Service test_service{
TestLogger{}
};
No large inheritance hierarchy is required merely to provide a mock.
Composition and Dependency Injection
A dependency can also be supplied from the outside:
class Service {
private:
Logger& logger_;
public:
explicit Service(Logger& logger)
: logger_{logger} {
}
};
Here, Service does not own the logger.
The relationship is more accurately described as:
Association / Dependency
Whereas:
class Service {
private:
Logger logger_;
};
represents:
Composition / Ownership
This distinction matters greatly in C++ because lifetime and ownership are central architectural concerns.
Composition and ECS
In game development, a traditional design might begin with:
GameObject
│
├── Player
├── Enemy
├── Vehicle
├── Bullet
└── NPC
But a player can instead be viewed as:
Player =
Position
+ Velocity
+ Health
+ Renderable
+ Input
An enemy:
Enemy =
Position
+ Velocity
+ Health
+ Renderable
+ AI
And a vehicle:
Vehicle =
Position
+ Velocity
+ Physics
+ Renderable
+ Engine
This way of thinking naturally leads toward:
Entity Component System — ECS
The entity receives its capabilities not from its position in an inheritance hierarchy, but from the components it contains.
Why Is Composition Often More Flexible?
Inheritance tends to bind:
Type Identity
+
Behavior
Composition allows us to separate:
What the object is
from:
What capabilities it has
Instead of creating a class named:
FastEncryptedCompressedNetworkStorage
we can build:
using MyStorage =
StorageEngine<
NetworkBackend,
ZstdCompression,
AES256Encryption,
FileLogger
>;
Then replace:
ZstdCompression
with:
Lz4Compression
without redesigning:
- the backend
- the logger
- the encryption layer
- the storage architecture
That is real architectural flexibility.
Composition Is Not Free
Just as inheritance can be overused, Composition can also be overused.
Excessive decomposition can produce:
- too many components
- unnecessary interfaces
- excessive indirection
- complex dependency wiring
- large numbers of template instantiations
- longer compilation times
- control flow that becomes difficult to follow
Therefore:
Good Composition does not mean creating the largest possible number of components.
Just as:
Good OOP does not mean creating the largest possible number of classes.
When Is Inheritance the Correct Solution?
Inheritance remains the natural choice when a real hierarchy exists.
For example:
Shape
├── Circle
├── Rectangle
└── Polygon
We can define:
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
Then:
class Circle final : public Shape {
public:
void draw() const override {
// Draw circle
}
};
class Rectangle final : public Shape {
public:
void draw() const override {
// Draw rectangle
}
};
And:
void render(const Shape& shape) {
shape.draw();
}
Here:
Circle IS-A Shape
Rectangle IS-A Shape
and polymorphic substitution is genuine.
Inheritance is therefore being used exactly where it belongs.
A Practical Test Before Using Inheritance
Before writing:
class B : public A {
};
ask:
Is B genuinely an A?
B IS-A A ?
Can B be used wherever A is expected?
For example:
void process(const A& object);
Would this be semantically correct?
B object;
process(object);
Do I actually need polymorphism?
If the objective is merely to reuse implementation from A, perhaps:
class B {
private:
A a_;
};
is better.
Or perhaps no class is needed at all.
Modern C++ Has Not Abandoned OOP
The modern view is not:
Inheritance = Bad
Composition = Good
The more useful principle is:
Use the weakest relationship that correctly models the design.
If the relationship is:
IS-A
use inheritance when a genuine hierarchy and substitution relationship exist.
If the relationship is:
HAS-A
use Composition.
If the relationship is:
USES-A
Dependency or Association may be more accurate.
And when the requirement is simply a capability or policy:
Templates + Concepts + Composition
may provide the most natural design.
Conclusion
Composition did not appear recently to defeat Inheritance.
It is an old design concept whose importance increased as decades of practical experience demonstrated that inheritance, despite its power, can create severe coupling when used merely for implementation reuse.
The real problem has never been inheritance itself.
The problem is:
Using a strong relationship such as Inheritance when a weaker and simpler relationship such as Composition is sufficient.
In modern C++, Composition is particularly powerful because it is no longer limited to:
class A {
B b_;
};
It can combine:
Objects
+
Value Semantics
+
RAII
+
Templates
+
Concepts
+
Policies
+
std::variant
+
Runtime Interfaces
This allows systems to be built from small, explicit, replaceable components instead of forcing every behavior into a single inheritance hierarchy.
The principle worth remembering is:
Use inheritance to model genuine hierarchy.
Use composition to assemble capabilities.
Or, in its simplest form:
IS-A → Inheritance
HAS-A → Composition
But the real strength of an advanced C++ programmer lies not merely in memorizing this rule.
It lies in understanding when the simplification stops being sufficient, when inheritance and composition should be used together, and when both should be replaced by value types, templates, algorithms, or free functions.
This is where the philosophy of modern C++ becomes most powerful:
The goal is not to choose one programming paradigm. The goal is to choose the simplest and strongest tool that expresses the problem with the least coupling, the clearest design, and the best performance appropriate to the system.
What did you think?
Sign in to react or comment.
Comments
0No comments yet.