C++ did not adopt OOP for elegance—it adopted it for scale and correctness.
You use OOP when:
- Multiple components share common behavior
- You want to separate interface from implementation
- You need runtime substitution (polymorphism)
- You want to extend behavior without rewriting existing code
Inheritance is the backbone that enables all of this—but it must be used deliberately, not mechanically.
Inheritance (`:`): Modeling an _Is-A_ Relationship
Inheritance allows a derived class to reuse and extend a base class.
+------------------+
| BaseClass |
|------------------|
| + baseNumber |
| + baseMethod() |
+---------^--------+
|
public inheritance
|
+------------------+
| DerivedClass |
|------------------|
| + derivedNumber |
| + derivedMethod()|
+------------------+
class BaseClass {
public:
int baseNumber;
};
class DerivedClass : public BaseClass {
public:
int derivedNumber;
};
When to Use Inheritance
Use inheritance only when:
Derivedis aBase(semantic relationship)- The base class represents a stable abstraction
- You want polymorphic substitution
Do not use inheritance just to reuse code, Prefer composition unless substitution is required
Types of Inheritance: Controlling the Public Contract
Inheritance is not just about behavior—it controls visibility and guarantees.
1. Public Inheritance (Recommended)
class Derived : public Base {};
Meaning:
“A `Derived` _is a_ `Base`”
public→publicprotected→protectedprivate→ not accessible
Use this for polymorphism and APIs
2. Protected Inheritance
class Derived : public Base {};
- Public and protected members become
protected - External users cannot treat Derived as Base
Rarely used, Useful in framework internals
3. Private Inheritance (Default)
class Derived : private Base {};
- Public/protected become
private - No “is-a” relationship
Not polymorphism, Implementation reuse only
Multiple & Multi-Level Inheritance
C++ supports:
- Multi-level inheritance (A → B → C)
- Multiple inheritance (C : A, B)
class A {};
class B {};
class C : public A, public B {};
When Multiple Inheritance Is Safe
- One base provides interface only
- Other provides implementation
- Virtual inheritance is well understood
Otherwise: high complexity, ambiguity, diamond problems
Initialization Lists: Correct Construction Order
C++ constructs base classes before derived classes.
class Base {
int value;
public:
Base(int v) : value(v) {
std::cout << "Base initialized: " << value << '\n';
}
};
class Derived : public Base {
int derivedValue;
public:
Derived(int baseVal, int derivedVal)
: Base(baseVal), derivedValue(derivedVal) {
std::cout << "Derived initialized: " << derivedValue << '\n';
}
};
Why Initialization Lists Matter
- Base classes must be constructed first
- Avoids default construction + reassignment
- Required for:
constmembers- references
- base classes without default constructors
Assigning inside constructor body is **incorrect design**
Constructor & Destructor Order (Critical for Resources)
Order is deterministic:
- Construction:
Base → Derived - Destruction:
Derived → Base
Construction order:
-------------------
Base()
|
v
Derived()
Destruction order:
------------------
Derived()
|
v
Base()
class Base {
public:
Base() { std::cout << "Base constructor\n"; }
~Base() { std::cout << "Base destructor\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor\n"; }
~Derived() { std::cout << "Derived destructor\n"; }
};
Output
Base constructor
Derived constructor
Derived destructor
Base destructor
Why This Matters
- Base resources must exist before derived uses them
- Derived must clean up before base is destroyed
If a base class is used polymorphically, its destructor must be virtual
Function Overriding: Runtime Polymorphism
Overriding allows a derived class to replace behavior.
class Animal {
public:
virtual void sound() {
std::cout << "Animal makes a sound\n";
}
};
class Dog : public Animal {
public:
void sound() override {
std::cout << "Dog barks\n";
}
};
When Overriding Is Needed
- Behavior depends on runtime type
- You call functions via base pointers or references
Animal
+-----------+
| sound() |
+-----^-----+
|
+-----------+
| Dog |
| sound() |
+-----------+
Animal* a = new Dog();
a->sound(); // Dog barks
Without virtual → wrong function is called
`virtual`: Dynamic Dispatch Explained
virtual tells the compiler:
“Decide which function to call **at runtime**, not compile time.”
Base* obj = new Derived();
obj->speak(); // Calls Derived::speak()
What Happens Internally:
- Object contains a vtable pointer
- Function lookup happens at runtime
- Small overhead, massive flexibility
Without virtual (Compile-time binding):
ptr ---> Base::speak()
With virtual (Runtime binding)
ptr
|
v
+------------------+
| vtable ptr |-----> Derived::speak()
+------------------+
Pure Virtual Functions & Abstract Classes (Interface Contract)
class Base {
public:
virtual void speak() = 0;
};
= 0→ pure virtual- Class becomes abstract
- Cannot be instantiated
Why abstract classes exist
To enforce _what must be implemented_, not _how_.
+------------------+
| AbstractBase |
|------------------|
| + speak() = 0 |
+---------^--------+
|
+------------------+
| Derived |
|------------------|
| + speak() |
+------------------+
AbstractBase b; // illegal
Derived d; // legal
class Derived : public Base {
public:
void speak() override {
std::cout << "Derived speaking\n";
}
};
When to Use Abstract Classes:
- Define interfaces
- Enforce implementation contracts
- Enable framework-style extensibility
`final`: Locking Design Decisions
final prevents further modification.
1. Prevent Overriding
Base::foo() [final]
|
X override forbidden
class Base {
public:
void foo() final;
};
2. Prevent Inheritance (No Inheritance)
class Utility final {};
class Utility final
|
X cannot derive
Why `final` Exists
- Enforce invariants
- Improve optimization
- Prevent unsafe extension
`virtual` and `final` **cannot be combined** on the same function
Design Rules You Should Remember
- Use inheritance for polymorphism, not reuse
- Always use
override - Base destructors must be
virtual - Prefer composition when substitution is not required
- Use
finalto protect stable APIs
OOP and Performance: The Truth
- Virtual calls cost one indirection
- Cost is negligible compared to:
- cache misses
- allocations
- synchronization
- Wrong architecture costs far more
Correct OOP improves maintainability, safety, and scalability, Premature optimization breaks systems
Composition vs Inheritance in C++
Designing Correct, Maintainable, and Performant Systems
Object-oriented design in C++ is not about choosing _features_—it is about choosing relationships.
The most common and most expensive mistake is using inheritance where composition is the correct tool.
This article explains when and why to use composition or inheritance, using ASCII diagrams, real C++ examples, and performance-aware reasoning.
The Core Question
Before writing : public Base, always ask:
Is this an "IS-A" relationship
or a "HAS-A" relationship?
- IS-A → inheritance
- HAS-A → composition
Most real systems are HAS-A.
Inheritance: Strong Coupling (IS-A)
What Inheritance Means
Derived IS-A Base
Derived can replace Base
+------------------+
| Vehicle |
|------------------|
| + move() |
+---------^--------+
|
public inheritance
|
+------------------+
| Car |
|------------------|
| + openTrunk() |
+------------------+
C++ Example
class Vehicle {
public:
virtual void move() {
std::cout << "Vehicle moves\n";
}
};
class Car : public Vehicle {
public:
void move() override {
std::cout << "Car drives\n";
}
};
When Inheritance Is Correct
- You need runtime polymorphism
- The base class is a stable abstraction
- Substitution must be valid everywhere
void transport(Vehicle& v) {
v.move();
}
Passing Car here must always be correct.
The Hidden Cost of Inheritance
Inheritance creates tight coupling.
Derived depends on:
- Base implementation
- Base lifetime rules
- Base virtual layout
- Base future changes
Fragile Base Class Problem
Base changes
|
v
Derived breaks silently
This is why inheritance hierarchies rot over time.
Composition: Flexible Design (HAS-A)
What Composition Means
Object CONTAINS another object, Behavior is delegated
+------------------+
| Car |
|------------------|
| + Engine engine |----+
| + drive() | |
+------------------+ |
v
+------------------+
| Engine |
|------------------|
| + start() |
| + stop() |
+------------------+
C++ Example
class Engine {
public:
void start() {
std::cout << "Engine started\n";
}
};
class Car {
Engine engine; // HAS-A relationship
public:
void drive() {
engine.start();
std::cout << "Car driving\n";
}
};
Why Composition Is Preferred
- No base-class fragility
- No virtual dispatch required
- Behavior can change dynamically
- Easier testing and reuse
Substitution vs Delegation (Key Difference)
Inheritance (Substitution)
Car can replace Vehicle
Vehicle* v = new Car();
v->move();
Composition (Delegation)
Car USES Engine
car.drive(); // delegates to engine
Composition does not promise substitutability — and that’s a feature.
Performance Perspective
Inheritance Cost
ptr -> vtable -> function
- One extra indirection
- Prevents inlining
- Harder for optimizer
Composition Cost
direct call -> inlineable
- Zero runtime overhead
- Better cache locality
- Better for hot paths
Composition is usually faster, Inheritance trades performance for flexibility
When Inheritance Is the Wrong Choice
- Reusing code
- Sharing data layout
- Avoiding duplication
- Convenience
class Logger {};
class FileLogger : public Logger {}; // likely wrong
Logger is a service, not a base type.
Composition + Interfaces (Best of Both Worlds)
Pattern: Interface + Composition
+------------------+
| Drawable (IF) |
|------------------|
| + draw() = 0 |
+---------^--------+
|
+------------------+
| Sprite |
|------------------|
| + Renderer r |
+------------------+
class Drawable {
public:
virtual void draw() = 0;
virtual ~Drawable() = default;
};
class Renderer {
public:
void render() {
std::cout << "Rendering\n";
}
};
class Sprite : public Drawable {
Renderer renderer;
public:
void draw() override {
renderer.render();
}
};
- Polymorphism where needed
- Composition for implementation
- Minimal inheritance depth
Decision Table
Need runtime polymorphism? → Inheritance
Need code reuse only? → Composition
Behavior varies at runtime? → Interface + Composition
Hot performance path? → Composition
Stable abstraction? → Inheritance
Rule of Thumb (Memorize This)
Prefer composition.
Use inheritance only for polymorphism.
Never inherit for reuse.
Architectural Summary Diagram
+------------------+
| Interface |
+---------^--------+
|
+------------------+
| Concrete Obj |
|------------------|
| composed parts |
+------------------+
This is how modern C++ systems are built.