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 Dog is a Mammal.
  • 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 Car has an Engine.

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 protected members) of the base class.
  • Polymorphism: Inheritance allows runtime dynamic dispatch via virtual functions, 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_ptr or std::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:

  1. A genuine "is-a" relationship exists, satisfying the Liskov Substitution Principle (any derived class can replace the base class without breaking the program).
  2. You need runtime polymorphism using base pointers or references.
  3. You are designing an interface hierarchy using pure virtual base classes (abstract classes).
  4. You need to override specific virtual hooks or methods provided by a framework.

When to Use Composition

Use composition when:

  1. A "has-a" or "uses-a" relationship better describes the domain model.
  2. You only need to reuse functionality from another class without inheriting its interface.
  3. You want to avoid deep, fragile class hierarchies.
  4. 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.