C++ OOP vs Composition: Inheritance, virtual dispatch, override, Interfaces & Dependency Injection

C++ OOP vs Composition: Inheritance, virtual dispatch, override, Interfaces & Dependency Injection
Table of Contents
- Inheritance vs Composition: The Fundamental Choice
- Virtual Functions and vtable Mechanics
- Pure Virtual Functions: C++ Interfaces
- override and final: Preventing Mistakes
- The virtual Destructor Rule
- Multiple Inheritance and the Diamond Problem
- Dependency Injection via Abstract Interfaces
- Policy-Based Design (CRTP): Zero-Cost Static Polymorphism
- std::variant as a Type-Safe Alternative to Hierarchy
- Real-World Design: Designing a Renderer
- Frequently Asked Questions
- Key Takeaway
Inheritance vs Composition: The Fundamental Choice
Virtual Functions and vtable Mechanics
When a class has virtual functions, the compiler creates a vtable (virtual dispatch table) — an array of function pointers. Each object of the class has a hidden vptr (vtable pointer) as its first member:
vtable layout (conceptual):
Virtual dispatch cost: One extra indirection (load vptr, load vtable entry, call). On modern in-order CPUs: ~1-3ns. In tight loops processing millions of objects, this is visible — use CRTP or std::variant for performance-critical polymorphism.
Pure Virtual Functions: C++ Interfaces
A class with at least one pure virtual function (= 0) is abstract — cannot be instantiated, only inherited from. This is C++'s interface mechanism:
override and final: Preventing Mistakes
Rule: Always use override on overriding functions. The compiler catches mismatched signatures that would be silent bugs without it.
The virtual Destructor Rule
Rule: Any class with virtual functions must have a virtual destructor. = default is sufficient if no custom cleanup is needed.
Multiple Inheritance and the Diamond Problem
Dependency Injection via Abstract Interfaces
Dependency Injection (DI) replaces hard-coded dependencies with injected interfaces — enabling testability, swappability, and composability:
Policy-Based Design (CRTP): Zero-Cost Static Polymorphism
CRTP (Curiously Recurring Template Pattern) provides compile-time polymorphism — no vtable, no virtual dispatch overhead:
Frequently Asked Questions
When is inheritance genuinely the right choice?
Use inheritance for: (1) implementing a behavioral contract defined by an abstract interface class, (2) adding a specialization with shared core behavior where "is-a" is genuinely true (a Dog is-an Animal), (3) CRTP for compile-time extensibility. Avoid for "sharing code" — use composition or free functions for that.
What is the NVI (Non-Virtual Interface) pattern?
NVI makes public functions non-virtual and virtual functions private/protected. Public non-virtual functions provide pre/post conditions, then call the virtual hook. This prevents derived classes from changing invariants enforced by the base's public interface. Example: std::ostream::operator<< is non-virtual; derived classes override virtual do_unshift.
Should I always use unique_ptr for polymorphic objects?
Yes — std::unique_ptr<IBase> with a virtual destructor is the standard pattern for polymorphic ownership. If multiple owners exist, use std::shared_ptr<IBase>. IBase* (raw pointer) is acceptable in non-owning observer roles when lifetime is managed elsewhere.
Key Takeaway
Modern C++ OOP is selective. Virtual dispatch (runtime polymorphism) is for open extension points where the type set grows at runtime. CRTP (static polymorphism) is for fixed type sets requiring zero overhead. Composition + dependency injection enables the flexibility of polymorphism without the fragility of deep hierarchies. Prefer interfaces (pure abstract classes) over concrete base classes to keep inheritance trees shallow and contracts clear.
Read next: Templates & Generic Programming: Logic Without Types →
Part of the C++ Mastery Course — 30 modules from modern C++ basics to expert systems engineering.
