SimplifyC++ Article

Inheritance in C++ Why It Evolved from a Central OOP Tool into a Feature That Should Be Used Carefully

By Ayman AlherakiReads: 43Today: 0

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:

Then build new classes on top of it:

The idea seems extremely attractive:

However, 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:

But these goals are not identical.

If we write:

we are not merely saying that FileStream reuses some implementation from Stream.

We are making a much stronger statement:

That 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:

rather than:

For example:

is conceptually wrong because a car is not an engine.

The more natural design is:

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:

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:

then 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:

Mathematically, 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:

Therefore, 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:

Heavy reliance on inheritance may lead to designs such as:

The hierarchy quickly becomes difficult to manage.

Composition allows these abilities to be modeled independently:

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:

a change near the top may affect every class below it.

The original promise may have been:

but the result can become:

A 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:

For example:

may result in:

where the Derived portion is lost.

Polymorphic use through a base pointer also requires proper destructor design:

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:

and:

For example, this can be an excellent design:

Then:

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:

Inheritance 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:

to:

That is a major architectural difference.

For example:

The engine may later become:

without 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:


Composition Is Not a Magic Solution Either

It would be equally wrong to move from:

to:

Composition 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:

ask:

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:


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:

The more mature design principle is therefore:

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.

Actual visitors 77,527
Visitors today 102
Total page views 1,718,409
Page views today 119
Book downloads 15,200