SimplifyC++ Article
Inheritance in C++ Why It Evolved from a Central OOP Tool into a Feature That Should Be Used Carefully
Inheritance in C++: Why It Evolved from a Central OOP Tool into a Feature That Should Be Used Carefully
From a Promising Code-Reuse Mechanism to the Modern Preference for Composition
When programmers first learn Object-Oriented Programming (OOP), inheritance often appears to be one of its most elegant ideas.
Instead of repeating code, we can create a base class:
class Animal {public: void eat(); void sleep();};Then build new classes on top of it:
class Dog : public Animal {public: void bark();};The idea seems extremely attractive:
Write common behavior once ↓Place it in a Base Class ↓Let other classes inherit itHowever, decades of experience with large software systems have shown that this picture is too simplistic.
The problem is not that inheritance has failed as a language feature. Rather, it has proven to be far less suitable as a general-purpose code-reuse mechanism than early OOP practice often suggested.
This is why one of the most widely repeated design principles became:
Favor Composition over Inheritance
In other words, when the primary goal is to reuse behavior or assemble components, composition is often the safer default.
Where Did the Problem Begin?
Inheritance has historically been used for three different purposes:
Inheritance │ ├── Subtyping ├── Polymorphism └── Code ReuseBut these goals are not identical.
If we write:
class FileStream : public Stream {};we are not merely saying that FileStream reuses some implementation from Stream.
We are making a much stronger statement:
FileStream IS-A StreamThat means any code that expects a Stream should be able to work correctly with a FileStream.
If the real relationship is merely that one object uses another, then the correct relationship is often:
HAS-Arather than:
IS-AFor example:
class Car : public Engine {};is conceptually wrong because a car is not an engine.
The more natural design is:
class Car { Engine engine_;};This is Composition.
Inheritance Creates Strong Coupling
When one class inherits from another, the relationship usually becomes much stronger than it initially appears.
A derived class may depend on:
the public interface,
virtual functions,
protected members,
construction and destruction rules,
base-class invariants,
and the way the base class internally invokes virtual behavior.
As a result, a seemingly small modification to a base class may break derived classes that the base-class author may not even know exist.
This is closely related to the well-known:
Fragile Base Class Problem
A base class becomes increasingly difficult to modify because many derived classes depend on its assumptions and internal behavior.
With composition, boundaries are usually clearer:
class Service { Logger logger_;};Service uses Logger, but it does not become a kind of Logger, nor does it automatically depend on its internal structure.
The Substitutability Problem
One of the most important requirements of public inheritance is that the derived class should be safely usable wherever the base class is expected.
This is the core idea behind the Liskov Substitution Principle.
If:
class D : public Bthen code expecting B should generally be able to use D without breaking the expected behavior.
But this requirement is much harder to satisfy than it may first appear.
A classic example is:
Circle IS-A EllipseMathematically, this is correct.
But suppose an Ellipse provides operations that independently modify width and height.
A Circle cannot naturally preserve that contract because it must always maintain:
width == heightTherefore, a mathematically valid classification does not automatically imply a valid software inheritance relationship.
Class Hierarchy Explosion
Inheritance works well when the domain being modeled is truly hierarchical.
However, many software capabilities are independent rather than hierarchical.
Consider a game where characters may:
WalkFlySwimHeavy reliance on inheritance may lead to designs such as:
WalkingCharacterFlyingCharacterSwimmingCharacterFlyingSwimmingCharacterWalkingFlyingCharacterWalkingFlyingSwimmingCharacter...The hierarchy quickly becomes difficult to manage.
Composition allows these abilities to be modeled independently:
class Character { Movement movement_; Weapon weapon_; Renderer renderer_;};Instead of inventing a new class for every possible combination, behavior can be assembled from independent components.
Inheritance Can Amplify Change
In a deep hierarchy:
Base ↓Level1 ↓Level2 ↓Level3 ↓Level4a change near the top may affect every class below it.
The original promise may have been:
Easier maintenancebut the result can become:
Change amplificationA small modification in the base class may require reviewing or repairing many derived classes.
Composition generally makes changes more local and isolated.
C++ Makes the Situation More Sensitive
These issues are not unique to C++. Similar problems appear in Java, C#, Python, and other object-oriented languages.
However, C++ adds its own complexities, including:
Object SlicingMultiple InheritanceVirtual InheritanceVirtual DestructorsConstruction / Destruction RulesObject LifetimeObject LayoutABI ConsiderationsFor example:
Derived d;Base b = d;may result in:
Object Slicingwhere the Derived portion is lost.
Polymorphic use through a base pointer also requires proper destructor design:
class Base {public: virtual ~Base() = default;};These details do not make inheritance inherently bad, but they increase the cost of using it correctly.
Does This Mean Inheritance Is No Longer Useful?
No.
There is an important distinction between:
Implementation Inheritanceand:
Interface InheritanceFor example, this can be an excellent design:
struct Device { virtual ~Device() = default;
virtual void read() = 0; virtual void write() = 0;};Then:
class SerialDevice : public Device {};
class NetworkDevice : public Device {};Here, inheritance is not primarily being used to steal implementation from Device.
Instead, it defines a common behavioral contract through which multiple concrete implementations can be used.
This remains one of the strongest and most legitimate uses of inheritance.
What Changed in Modern C++?
Modern C++ provides many alternatives for problems that were once routinely solved through inheritance:
CompositionTemplatesConceptsCRTPPolicy Classesstd::variantstd::functionType ErasureDependency InjectionInheritance is therefore no longer the default path for polymorphism or behavior reuse.
Modern C++ gives designers a broader set of tools, allowing each problem to be modeled according to its actual requirements instead of forcing everything into a class hierarchy.
Why Has Composition Become the Default in Many Designs?
Because composition changes the relationship from:
I am youto:
I use youThat is a major architectural difference.
For example:
class Car { Engine engine_;};The engine may later become:
PetrolEngineDieselEngineElectricMotorHybridPowertrainwithout changing the conceptual identity of Car.
Composition allows behavior to remain an independent component that can be replaced, tested, configured, or evolved separately.
This makes it particularly suitable for:
Dependency InjectionTestingMockingRuntime ConfigurationIndependent DevelopmentComposition Is Not a Magic Solution Either
It would be equally wrong to move from:
Inheritance everywhereto:
Never use inheritanceComposition can increase the number of components, delegation layers, and configuration logic.
Inheritance still works very well when there is a genuine and stable subtype relationship.
Therefore, the correct principle is not:
Never use inheritance.
It is:
Do not use inheritance merely because you want to reuse some code.
A Practical Rule for C++ Developers
Before writing:
class D : public Bask:
Is D truly a B?
Can D safely replace B without violating expected behavior?
Do I genuinely need runtime polymorphism?
Will this relationship still make sense after years of system evolution?If the main reason is simply:
I want to reuse some of B's code.
then inheritance is probably not the best choice.
The better design may be:
class D { B b_;};Conclusion
Inheritance has not failed in C++, nor has it become obsolete.
What has changed is the old belief that inheritance should be treated as a general-purpose mechanism for code reuse.
Long experience has shown that excessive use of implementation inheritance can easily lead to:
Tight CouplingFragile Base ClassesBroken SubstitutabilityDeep HierarchiesState Management ProblemsMaintenance ComplexityThe more mature design principle is therefore:
Prefer Composition for reuse.
Use Inheritance for genuine subtype relationships.Composition says:
This object owns or uses this capability.
Public inheritance says:
This object truly is a kind of that type and can safely replace it.
The second statement is far stronger.
That is why it should be made far more carefully.
Perhaps the greatest misunderstanding surrounding inheritance is that programmers often learn it early as a convenient way to save a few lines of code, while its real nature becomes clear only much later:
Inheritance is not primarily a code-reduction mechanism. It is an architectural declaration of a strong relationship between types.
What did you think?
Sign in to react or comment.
Comments
0No comments yet.