C++ - Object-Oriented Programming & Polymorphism
In C++, Object-Oriented Programming (OOP) and polymorphism are not limited to traditional inheritance and virtual methods. C++ provides both runtime (dynamic) and compile-time (static) polymorphic mechanisms to balance flexibility, memory layout, and runtime performance.
1. Dynamic Polymorphism: virtual Functions & Vtables
The classical OOP approach relies on class hierarchies, abstract base classes, and virtual member functions.
#include <iostream>
#include <memory>
class Shape {
public:
// Pure virtual method (interface contract)
virtual void draw() const = 0;
// Rule of thumb: Base classes with virtual functions MUST have a virtual destructor
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override { // 'override' ensures signature matches base class
std::cout << "Drawing Circle\n";
}
};
class Square : public Shape {
public:
void draw() const final { // 'final' prevents further derived overrides
std::cout << "Drawing Square\n";
}
};
How Vtables Work Under the Hood
When a class declares or inherits a virtual function, the compiler inserts a hidden pointer called vptr (virtual pointer) into each object instance. The vptr points to a shared table of function pointers called the vtable (virtual table).
Object Instance (Stack / Heap) Class Vtable (Read-Only Data Segment)
+-----------------------+ +-----------------------+
| vptr (8 bytes) | ------------> | &Circle::draw() |
| radius (4 bytes) | | &Circle::~Circle() |
+-----------------------+ +-----------------------+
- Overhead: Each virtual call involves an indirect pointer dereference (
object->vptr[index]()), preventing compiler function inlining and instruction cache prefetching. - Size Cost: Every object carries an extra 8-byte pointer (on 64-bit systems).
2. Static Polymorphism: Curiously Recurring Template Pattern (CRTP)
CRTP achieves polymorphic behavior at compile time without virtual tables, pointer indirections, or memory bloat.
The derived class inherits from a templated base class, passing itself as the template argument:
#include <iostream>
template <typename Derived>
class BaseShape {
public:
void draw() const {
// Cast base pointer to derived type at compile time
static_cast<const Derived*>(this)->drawImpl();
}
};
class Circle : public BaseShape<Circle> {
public:
void drawImpl() const {
std::cout << "CRTP Circle: Draw\n";
}
};
class Square : public BaseShape<Square> {
public:
void drawImpl() const {
std::cout << "CRTP Square: Draw\n";
}
};
template <typename T>
void renderShape(const BaseShape<T>& shape) {
shape.draw(); // Resolved and inlined at compile time
}
- Advantages: Zero runtime cost, function calls can be fully inlined by the optimizer.
- Limitation: Cannot store heterogeneous types in a single
std::vector<BaseShape>without wrapping.
3. Value-Based Polymorphism: std::variant & std::visit (C++17)
When the set of types is known in advance (closed set), std::variant provides type-safe, contiguous, stack-allocated polymorphism without pointer indirection.
#include <iostream>
#include <variant>
#include <vector>
struct Circle { double radius; };
struct Square { double side; };
// Overloaded visitor helper
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
using Shape = std::variant<Circle, Square>;
int main() {
std::vector<Shape> shapes = { Circle{5.0}, Square{10.0} };
for (const auto& shape : shapes) {
std::visit(overloaded {
[](const Circle& c) { std::cout << "Circle r=" << c.radius << "\n"; },
[](const Square& s) { std::cout << "Square a=" << s.side << "\n"; }
}, shape);
}
}
- Advantages: Cache-friendly, value semantics (no manual heap allocations or
std::unique_ptr).
4. Type Erasure: std::function & Custom Wrappers
Type erasure decouples the polymorphic interface from inheritance hierarchies while allowing value semantics:
- Standard Library Examples:
std::function<void()>wraps any callable (function pointer, lambda, functor) without common base inheritance. - Custom Type Erasure: Combines an internal templated implementation struct with an external non-templated wrapper (similar to
std::anyorstd::shared_ptrdeleters).
Polymorphism Comparison
| Technique | Dispatch Time | Memory Layout | Inlining Possible? | Dynamic Extensibility? |
|---|---|---|---|---|
virtual Functions |
Runtime (Vtable) | Object + 8B vptr |
Rarely | Yes (Open hierarchy) |
| CRTP / Templates | Compile-Time | Zero extra overhead | Yes | No (Closed at compile time) |
std::variant |
Runtime (Tag check) | Max size + discriminator | Partial | No (Closed type list) |
| Type Erasure | Runtime (Function ptrs) | Heap or Small Buffer | No | Yes (Any matching type) |

