Inheritance, Polymorphism, and Virtual Basics: How C++ Expresses Substitutable Behavior
Inheritance and polymorphism in C++ are utilized to express that "different types can be manipulated via a unified interface." They are absolutely not a universal toolkit for lazy code reuse. Inheritance erects rigid type hierarchies, virtual functions trigger runtime dispatch overhead, and base class destruction profoundly impacts resource release integrity. In the beginner phase, you must first comprehend substitutable behavior before hastily deciding if you actually need inheritance.
Base Classes and Derived Classes
class Shape {
public:
double x = 0;
double y = 0;
};
class Circle : public Shape {
public:
double radius = 1;
};
Circle inherits from Shape.
It acquires Shape's public members, and introduces its own radius.
Circle c;
c.x = 10;
c.radius = 5;
This specific inheritance expresses an "is-a" relationship: "A Circle is a Shape." If the relationship is merely "contains a Shape," composition is almost certainly the superior choice.
public Inheritance Expresses Is-A
public inheritance dictates that a derived class object can be seamlessly substituted anywhere a base class object is expected.
void move_to_origin(Shape& shape) {
shape.x = 0;
shape.y = 0;
}
Circle circle;
move_to_origin(circle);
This substitution is only logically sound if the derived class strictly fulfills the base class's entire contract.
If your sole motivation is to reuse a few data fields, do not invoke public inheritance.
Member Access Control
| Access Control | External Access | Derived Class Access |
|---|---|---|
public |
Yes | Yes |
protected |
No | Yes |
private |
No | No |
protected is rarely the correct default choice.
It tightly couples the derived class to the base class's internal implementation details.
Consequently, altering the base class internals will violently cascade into every single derived class.
Virtual Functions Implement Runtime Polymorphism
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override {
return 3.14159 * radius_ * radius_;
}
private:
double radius_;
};
area is a virtual function.
When invoked via a base class reference or pointer, the runtime system dynamically selects the implementation corresponding to the object's true, concrete type.
void print_area(const Shape& shape) {
std::cout << shape.area() << '\n';
}
This is the essence of polymorphism.
Pure Virtual Functions Define Interfaces
= 0 designates a pure virtual function.
A class containing pure virtual functions generally cannot be instantiated directly; it is an abstract base class.
class Renderer {
public:
virtual ~Renderer() = default;
virtual void render() = 0;
};
It expresses a pure interface. Concrete classes implement this interface.
class OpenGLRenderer : public Renderer {
public:
void render() override;
};
Interfaces must be small and brutally stable. Do not dump a massive pile of unrelated capabilities into a single monolithic base class.
override Prevents Signature Mismatches
class Circle : public Shape {
public:
double area() const override;
};
override enforces a strict contract with the compiler: this function must override an existing virtual function in the base class.
If you mistype the signature, the compiler will aggressively reject the build.
Without override, a slight typo might accidentally define a brand new function, silently destroying the intended polymorphic behavior.
Engineering code must mandate the use of override.
Base Class Destructors Must Be virtual
If there is any possibility that a derived object will be deleted via a base class pointer, the base class destructor must be virtual.
class Base {
public:
virtual ~Base() = default;
};
If it isn't:
Base* p = new Derived();
delete p;
The system may only invoke the base class destructor, silently failing to release the derived class's resources. This is an catastrophic resource leak vector.
Object Slicing
Passing a derived object by value to a function expecting a base class object triggers slicing.
void draw(Shape shape); // High Risk!
When passing a Circle, only the Shape sub-object is duplicated.
All dynamic type information is permanently annihilated.
Polymorphic objects must always be passed via references, raw pointers, or smart pointers.
void draw(const Shape& shape);
Prefer Composition Over Inheritance
If the relationship is not "is a", but rather "has a", strictly utilize composition.
class Window {
public:
void draw();
private:
Renderer& renderer_;
};
Composition makes dependencies blazing clear. Inheritance violently forces both the base class's interface and its implementation details into the derived class. The vast majority of code reuse scenarios do not actually require inheritance.
final Halts Derivation
class Circle final : public Shape {
public:
double area() const override;
};
final unequivocally states that this class can no longer be inherited from.
It establishes hard architectural boundaries and can significantly aid the optimizer in devirtualization.
However, do not blindly slap final onto public extension points where future derivation is legitimate.
Keep Abstraction Hierarchies Shallow
Deep inheritance hierarchies are a nightmare:
- Construction and destruction order becomes indecipherable.
- Virtual function override chains are impossible to audit.
- Base class modifications have a massive, explosive blast radius.
- Test doubles (mocks/stubs) become hopelessly complex.
- The ABI becomes deeply unstable.
Prioritize tiny interfaces and composition. Do not model your type hierarchy like a biological family tree.
Polymorphism and Performance
Virtual function calls typically mandate an indirect jump instruction. This can sabotage function inlining and branch prediction. However, for the vast majority of business logic, you should not prematurely optimize this away.
Performance-critical hot paths must be evaluated using a profiler. Alternatives include:
- Standard virtual functions.
- Template-based static polymorphism (CRTP).
std::variant.- Function objects/lambdas.
- Hand-rolled function pointer tables.
The correct abstraction mechanism depends entirely on whether the type set is open, whether it crosses an ABI boundary, and whether the indirect call actually bottlenecks performance.
Engineering Risks
Common risks plaguing inheritance and polymorphism:
- Base class destructors lacking
virtual. - Omitting the
overridekeyword, causing silent dispatch failures. - Invoking virtual functions inside constructors/destructors (which do not dispatch to the derived class as you might expect).
- Object slicing caused by passing polymorphic types by value.
- Abusing inheritance for mere implementation reuse rather than expressing a true "is-a" relationship.
- Using
protectedto expose far too much internal state to subclasses. - Building impossibly deep hierarchies that defy auditing.
- Exposing C++ polymorphic classes across dynamic library boundaries, inciting severe ABI instability.
These architectural fractures will be thoroughly dissected in the upcoming articles covering vtables and ABIs.
Summary
Inheritance expresses a rigorous type substitution relationship. Virtual functions express behavior that is replaceable at runtime. Polymorphism liberates your code to depend on stable interfaces rather than volatile concrete implementations. However, inheritance simultaneously introduces profound risks regarding lifecycle management, resource release integrity, performance overhead, and ABI fragility. The safest foundational rule in C++ engineering is: compose whenever possible; only reach for virtual interfaces when you absolutely demand open, runtime extensible behavior.