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::any or std::shared_ptr deleters).

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)