Composition vs Inheritance in C++
This article explores the fundamental differences between composition and inheritance when designing object-oriented systems in C++. It examines how both paradigms facilitate code reuse, compares their relationship models ("is-a" versus "has-a"), analyzes the structural trade-offs regarding coupling and encapsulation, and provides clear criteria for deciding which approach to implement in your software architecture.
Core Conceptual Differences
In C++, both inheritance and composition enable code reuse and structural design, but they achieve these goals through fundamentally different relationships:
- Inheritance ("is-a" relationship): A derived class
automatically acquires the members, methods, and interface of a base
class. For example, a
Dogis aMammal. - Composition ("has-a" relationship): A class
contains one or more instances of other classes as private or protected
member variables to leverage their capabilities. For example, a
Carhas anEngine.
Inheritance in C++
Inheritance is defined syntactically through derivation:
class Engine {
public:
void start() { /* ... */ }
};
// Derived via inheritance
class Car : public Engine {
// Car inherits start(), exposing it as part of Car's interface
};Characteristics of Inheritance:
- White-Box Reuse: The derived class often has
visibility into the internal workings (such as
protectedmembers) of the base class. - Polymorphism: Inheritance allows runtime dynamic
dispatch via
virtualfunctions, enabling a base class pointer or reference to manipulate different derived objects uniformly. - Static Binding: The relationship between the base and derived classes is fixed at compile-time and cannot be altered at runtime.
- Tight Coupling: Any change to the base class's interface or internal behavior propagates to all derived classes, potentially breaking downstream code.
Composition in C++
Composition constructs complex types by aggregating simpler types:
class Engine {
public:
void start() { /* ... */ }
};
// Assembled via composition
class Car {
private:
Engine engine; // Car has an Engine
public:
void drive() {
engine.start();
}
};Characteristics of Composition:
- Black-Box Reuse: The host class only interacts with the contained object through its public interface, preserving encapsulation.
- Loose Coupling: The internal implementation of the component class can change without directly breaking the container class, provided the public interface remains intact.
- Dynamic Flexibility: Composition can use pointers
(such as
std::unique_ptrorstd::shared_ptr) to abstract interfaces, allowing components to be swapped out dynamically at runtime. - Explicit Interface Delegation: The containing class must explicitly delegate calls to its member objects, avoiding unintended exposure of internal functions.
Key Comparisons
| Feature | Inheritance | Composition |
|---|---|---|
| Relationship | "Is-a" | "Has-a" |
| Coupling Level | Tight (high dependency) | Loose (low dependency) |
| Flexibility | Static (fixed at compile time) | Dynamic (can change at runtime) |
| Encapsulation | Weak (derived class sees internals) | Strong (member internals are hidden) |
| Polymorphism | Enabled natively via virtual tables | Implemented via dependency injection |
When to Use Inheritance
Use inheritance when:
- A genuine "is-a" relationship exists, satisfying the Liskov Substitution Principle (any derived class can replace the base class without breaking the program).
- You need runtime polymorphism using base pointers or references.
- You are designing an interface hierarchy using pure virtual base classes (abstract classes).
- You need to override specific virtual hooks or methods provided by a framework.
When to Use Composition
Use composition when:
- A "has-a" or "uses-a" relationship better describes the domain model.
- You only need to reuse functionality from another class without inheriting its interface.
- You want to avoid deep, fragile class hierarchies.
- You need the flexibility to change the underlying implementation or behavior of a component at runtime.
A central principle of modern C++ design is to favor composition over inheritance. While inheritance provides specialized tools for polymorphism and interface realization, composition yields systems that are simpler to test, maintain, and refactor.