Docs

README

Polymorphism in C++

Table of Contents

  1. What is Polymorphism
  2. Compile-time vs Runtime
  3. Virtual Functions
  4. How vtable Works (Under the Hood)
  5. Override and Final
  6. Pure Virtual and Abstract Classes
  7. Virtual Destructor
  8. Dynamic Casting
  9. Best Practices

What is Polymorphism

Polymorphism = "many forms" - the ability of objects to take on different forms and behave differently based on their actual type, even when accessed through a common interface.

Shape* shape = new Circle();  // shape points to a Circle
shape->draw();  // Calls Circle::draw(), not Shape::draw() ✓

The Power of Polymorphism Visualized

                    ┌─────────────────────────────────────────────────┐
                    │              Shape* shape                        │
                    │         (Pointer to Base Class)                  │
                    └───────────────────┬─────────────────────────────┘
                                        │
                                        │ Can point to ANY derived class
                                        │
        ┌───────────────────────────────┼───────────────────────────────┐
        │                               │                               │
        ▼                               ▼                               ▼
  ┌───────────┐                   ┌───────────┐                   ┌───────────┐
  │  Circle   │                   │ Rectangle │                   │ Triangle  │
  │  ●        │                   │  ┌──────┐ │                   │    △      │
  │           │                   │  │      │ │                   │   /  \    │
  │ draw() ●  │                   │ draw() □ │                   │ draw() △  │
  └───────────┘                   └───────────┘                   └───────────┘
        │                               │                               │
        │                               │                               │
        ▼                               ▼                               ▼
   "Drawing                        "Drawing                        "Drawing
    a circle"                       a rectangle"                    a triangle"

Same Code:                    shape->draw();
Different Behavior:           Based on actual object type at runtime

Types of Polymorphism

TypeAlso CalledMechanismWhen Resolved
Compile-timeStatic PolymorphismFunction/Operator Overloading, TemplatesCompile Time
RuntimeDynamic PolymorphismVirtual Functions + InheritanceRuntime

Compile-time vs Runtime

Compile-time (Static) Polymorphism

Resolved during compilation - the compiler knows exactly which function to call.

// 1. FUNCTION OVERLOADING
void print(int x)    { cout << "Integer: " << x << endl; }
void print(double x) { cout << "Double: " << x << endl; }
void print(string s) { cout << "String: " << s << endl; }

print(42);        // Calls print(int)      - decided at compile time
print(3.14);      // Calls print(double)   - decided at compile time
print("hello");   // Calls print(string)   - decided at compile time

// 2. TEMPLATES (Parametric Polymorphism)
template<typename T>
T add(T a, T b) { return a + b; }

add(1, 2);        // Compiler generates add<int>()
add(1.5, 2.5);    // Compiler generates add<double>()

// 3. OPERATOR OVERLOADING
class Vector2D {
public:
    float x, y;
    Vector2D operator+(const Vector2D& other) const {
        return {x + other.x, y + other.y};
    }
};

Runtime (Dynamic) Polymorphism

Resolved during program execution - uses virtual functions and inheritance.

class Animal {
public:
    virtual void speak() { cout << "..." << endl; }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!" << endl; }
};

class Cat : public Animal {
public:
    void speak() override { cout << "Meow!" << endl; }
};

// The magic of runtime polymorphism:
void makeSpeak(Animal* animal) {
    animal->speak();  // Which speak()? Decided at RUNTIME!
}

int main() {
    Animal* dog = new Dog();
    Animal* cat = new Cat();

    makeSpeak(dog);  // "Woof!" - determined by actual object type
    makeSpeak(cat);  // "Meow!" - determined by actual object type

    delete dog;
    delete cat;
}

Visual Comparison

┌────────────────────────────────────────────────────────────────────────────┐
│                     COMPILE-TIME vs RUNTIME POLYMORPHISM                   │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  COMPILE-TIME (Static)                                                      │
│  ─────────────────────                                                      │
│                                                                             │
│    Source Code                    Compiled Binary                           │
│    ┌──────────────┐              ┌──────────────────────────┐              │
│    │ print(42)    │  ────────►   │ call print_int           │              │
│    │ print(3.14)  │  ────────►   │ call print_double        │              │
│    │ print("hi")  │  ────────►   │ call print_string        │              │
│    └──────────────┘              └──────────────────────────┘              │
│                                                                             │
│    Decision made: During COMPILATION                                        │
│    Pros: Fast (no runtime overhead)                                         │
│    Cons: Less flexible, code must be known at compile time                  │
│                                                                             │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  RUNTIME (Dynamic)                                                          │
│  ────────────────                                                           │
│                                                                             │
│    Source Code                    At Runtime                                │
│    ┌──────────────┐              ┌──────────────────────────┐              │
│    │ p->speak()   │              │ Check p's vtable         │              │
│    │              │  ────────►   │ Look up speak() address  │              │
│    │              │              │ Jump to correct function │              │
│    └──────────────┘              └──────────────────────────┘              │
│                                                                             │
│    Decision made: During EXECUTION                                          │
│    Pros: Flexible, extensible, works with unknown types                     │
│    Cons: Slight overhead (vtable lookup)                                    │
│                                                                             │
└────────────────────────────────────────────────────────────────────────────┘

Virtual Functions

The virtual keyword enables runtime polymorphism by telling the compiler to use dynamic dispatch.

Syntax and Basic Example

class Base {
public:
    virtual void method() {  // ◄── 'virtual' keyword
        cout << "Base::method()" << endl;
    }
    virtual ~Base() = default;  // Always make destructor virtual!
};

class Derived : public Base {
public:
    void method() override {  // ◄── 'override' keyword (C++11)
        cout << "Derived::method()" << endl;
    }
};

// Polymorphic behavior in action
Base* ptr = new Derived();
ptr->method();  // Output: "Derived::method()" ✓
delete ptr;

Non-virtual vs Virtual: The Difference

class Base {
public:
    void nonVirtual() { cout << "Base::nonVirtual" << endl; }
    virtual void isVirtual() { cout << "Base::isVirtual" << endl; }
};

class Derived : public Base {
public:
    void nonVirtual() { cout << "Derived::nonVirtual" << endl; }
    void isVirtual() override { cout << "Derived::isVirtual" << endl; }
};

Base* ptr = new Derived();
ptr->nonVirtual();  // "Base::nonVirtual"      ◄── Static binding (pointer type)
ptr->isVirtual();   // "Derived::isVirtual"    ◄── Dynamic binding (object type)
delete ptr;
┌────────────────────────────────────────────────────────────────────────┐
│                    STATIC vs DYNAMIC BINDING                           │
├────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Base* ptr = new Derived();                                             │
│        │             │                                                  │
│        │             └─── Actual object type: Derived                   │
│        │                                                                │
│        └─── Pointer type: Base*                                         │
│                                                                         │
│  NON-VIRTUAL (Static Binding):                                          │
│  ─────────────────────────────                                          │
│    ptr->nonVirtual()                                                    │
│         │                                                               │
│         └── Compiler sees: "ptr is Base*"                               │
│             Decision: Call Base::nonVirtual()                           │
│             When: Compile time                                          │
│                                                                         │
│  VIRTUAL (Dynamic Binding):                                             │
│  ──────────────────────────                                             │
│    ptr->isVirtual()                                                     │
│         │                                                               │
│         └── Runtime sees: "Object is actually Derived"                  │
│             Decision: Call Derived::isVirtual()                         │
│             When: Runtime (via vtable lookup)                           │
│                                                                         │
└────────────────────────────────────────────────────────────────────────┘

How vtable Works (Under the Hood)

Every class with virtual functions has a hidden pointer called vptr (virtual pointer) that points to a vtable (virtual function table).

vtable Structure

┌─────────────────────────────────────────────────────────────────────────────┐
│                           VTABLE MECHANISM                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  class Base {                          BASE VTABLE                           │
│      virtual void foo();               ┌──────────────────────────────┐     │
│      virtual void bar();               │ [0] → Base::foo() address    │     │
│  };                                    │ [1] → Base::bar() address    │     │
│                                        └──────────────────────────────┘     │
│                                                                              │
│  class Derived : public Base {         DERIVED VTABLE                        │
│      void foo() override;              ┌──────────────────────────────┐     │
│      // bar() not overridden           │ [0] → Derived::foo() address │◄─── │
│  };                                    │ [1] → Base::bar() address    │     │
│                                        └──────────────────────────────┘     │
│                                              Changed!    │                   │
│                                                          │                   │
│                                                          │                   │
│  OBJECT MEMORY LAYOUT                                    │                   │
│  ─────────────────────                                   │                   │
│                                                          │                   │
│  Base object:                                            │                   │
│  ┌─────────────────────┐                                 │                   │
│  │ vptr ──────────────────► Base vtable                  │                   │
│  │ Base data members   │                                 │                   │
│  └─────────────────────┘                                 │                   │
│                                                          │                   │
│  Derived object:                                         │                   │
│  ┌─────────────────────┐                                 │                   │
│  │ vptr ──────────────────► Derived vtable ──────────────┘                   │
│  │ Base data members   │                                                     │
│  │ Derived data members│                                                     │
│  └─────────────────────┘                                                     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Virtual Function Call Step-by-Step

Base* ptr = new Derived();
ptr->foo();

STEP-BY-STEP EXECUTION:
══════════════════════

Step 1: Access object through ptr
        ┌──────────────────────────────────────┐
        │  ptr ──────────► Derived Object      │
        │                  ┌──────────────┐    │
        │                  │ vptr ────┐   │    │
        │                  │ data     │   │    │
        │                  └──────────────┘    │
        └──────────────────────────────────────┘

Step 2: Follow vptr to vtable
        ┌──────────────────────────────────────┐
        │                      │               │
        │                      ▼               │
        │              Derived vtable          │
        │              ┌──────────────────┐    │
        │              │[0] Derived::foo()│    │
        │              │[1] Base::bar()   │    │
        │              └──────────────────┘    │
        └──────────────────────────────────────┘

Step 3: Look up function at index 0 (foo's slot)
        ┌──────────────────────────────────────┐
        │              │                       │
        │              ▼                       │
        │     Derived::foo() ◄── Call this!   │
        │              │                       │
        │              ▼                       │
        │     Execute function code            │
        └──────────────────────────────────────┘

Complete Example with Memory Layout

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() { cout << "..." << endl; }
    virtual void move() { cout << "Moving" << endl; }
    virtual ~Animal() = default;
protected:
    int age = 0;
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!" << endl; }
    // move() is NOT overridden - uses Animal::move()
private:
    string breed;
};

class Cat : public Animal {
public:
    void speak() override { cout << "Meow!" << endl; }
    void move() override { cout << "Prowling" << endl; }  // Overrides!
};
MEMORY LAYOUT AND VTABLES:

Animal vtable:              Dog vtable:                 Cat vtable:
┌────────────────────┐      ┌────────────────────┐      ┌────────────────────┐
│ [0] Animal::speak  │      │ [0] Dog::speak     │      │ [0] Cat::speak     │
│ [1] Animal::move   │      │ [1] Animal::move   │      │ [1] Cat::move      │
│ [2] Animal::~dtor  │      │ [2] Dog::~dtor     │      │ [2] Cat::~dtor     │
└────────────────────┘      └────────────────────┘      └────────────────────┘

Animal object:              Dog object:                  Cat object:
┌─────────────────┐         ┌─────────────────┐         ┌─────────────────┐
│ vptr ───────────┼──►      │ vptr ───────────┼──►      │ vptr ───────────┼──►
│ age: 0          │         │ age: 0          │         │ age: 0          │
└─────────────────┘         │ breed: ""       │         └─────────────────┘
                            └─────────────────┘

Code:
  Animal* animals[] = {new Animal, new Dog, new Cat};
  for (auto a : animals) {
      a->speak();  // "..." → "Woof!" → "Meow!"
      a->move();   // "Moving" → "Moving" → "Prowling"
  }

Override and Final

The override Keyword (C++11)

The override keyword tells the compiler to verify that the function actually overrides a virtual function from the base class. This catches bugs at compile time!

class Base {
public:
    virtual void foo() { }
    virtual void bar(int x) { }
    void baz() { }  // NOT virtual
};

class Derived : public Base {
public:
    void foo() override { }       // ✅ OK - correctly overrides Base::foo

    // void foo() const override { }  // ❌ ERROR: signature mismatch (const)
    // void bar() override { }        // ❌ ERROR: signature mismatch (no int)
    // void baz() override { }        // ❌ ERROR: Base::baz is not virtual
    // void qux() override { }        // ❌ ERROR: no Base::qux to override
};
┌─────────────────────────────────────────────────────────────────────────────┐
│                   WHY USE override?  - CATCHES BUGS!                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  WITHOUT override:                                                           │
│  ─────────────────                                                           │
│                                                                              │
│    class Base {                                                              │
│        virtual void process(int x) { }                                       │
│    };                                                                        │
│                                                                              │
│    class Derived : public Base {                                             │
│        void process(double x) { }  // OOPS! Different parameter type        │
│    };                              // Creates NEW function, doesn't override │
│                                    // Compiles without warning!              │
│                                                                              │
│  WITH override:                                                              │
│  ──────────────                                                              │
│                                                                              │
│    class Derived : public Base {                                             │
│        void process(double x) override { }  // ❌ COMPILER ERROR!           │
│    };                                       // "does not override"          │
│                                                                              │
│    BEST PRACTICE: Always use 'override' when overriding virtual functions   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

The final Keyword (C++11)

Prevents further overriding of a virtual function or inheritance from a class.

// 1. Final on a virtual function - cannot be overridden further
class Base {
public:
    virtual void foo() { }
};

class Derived : public Base {
public:
    void foo() final { }  // ◄── No class can override this anymore
};

class MoreDerived : public Derived {
public:
    // void foo() override { }  // ❌ ERROR: foo is final in Derived
};

// 2. Final on a class - cannot be inherited from
class Singleton final {  // ◄── No class can inherit from Singleton
public:
    static Singleton& getInstance();
};

// class BadIdea : public Singleton { };  // ❌ ERROR: Singleton is final

Visual Decision Guide

┌─────────────────────────────────────────────────────────────────┐
│                 WHEN TO USE override AND final                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  override:                                                       │
│  ──────────                                                      │
│    ✅ ALWAYS use when you intend to override                    │
│    ✅ Catches typos and signature mismatches                    │
│    ✅ Makes code self-documenting                               │
│                                                                  │
│  final on function:                                              │
│  ──────────────────                                              │
│    ✅ When override behavior must not change in subclasses      │
│    ✅ When further overriding would break invariants            │
│    ✅ Can enable compiler optimizations (devirtualization)      │
│                                                                  │
│  final on class:                                                 │
│  ───────────────                                                 │
│    ✅ Singletons and similar patterns                           │
│    ✅ Classes not designed for inheritance                      │
│    ✅ Security-critical classes                                 │
│    ✅ Can enable compiler optimizations                         │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Pure Virtual and Abstract Classes

Pure Virtual Function

class Shape {
public:
    virtual void draw() = 0;  // Pure virtual
    virtual double area() = 0;
};
// Shape s;  // ERROR: cannot instantiate abstract class

Abstract Class

class Animal {
public:
    virtual void speak() = 0;
    virtual void move() = 0;

    void breathe() {  // Can have regular methods
        cout << "Breathing..." << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!" << endl; }
    void move() override { cout << "Running" << endl; }
};

Interface Pattern

class IDrawable {
public:
    virtual void draw() = 0;
    virtual ~IDrawable() = default;
};

class IClickable {
public:
    virtual void onClick() = 0;
    virtual ~IClickable() = default;
};

class Button : public IDrawable, public IClickable {
public:
    void draw() override { /* ... */ }
    void onClick() override { /* ... */ }
};

Virtual Destructor

Why It's Essential

class Base {
public:
    ~Base() { cout << "~Base" << endl; }  // Non-virtual: PROBLEM!
};

class Derived : public Base {
    int* data;
public:
    Derived() : data(new int[100]) {}
    ~Derived() { delete[] data; cout << "~Derived" << endl; }
};

Base* ptr = new Derived();
delete ptr;  // Only calls ~Base! Memory leak!

The Fix

class Base {
public:
    virtual ~Base() { cout << "~Base" << endl; }
};

class Derived : public Base {
public:
    ~Derived() override { cout << "~Derived" << endl; }
};

Base* ptr = new Derived();
delete ptr;  // Calls ~Derived then ~Base ✓

Rule: If a class has any virtual functions, make destructor virtual.


Dynamic Casting

Syntax

Derived* d = dynamic_cast<Derived*>(basePtr);
if (d) {
    // Cast succeeded
}

Example

class Animal { public: virtual ~Animal() {} };
class Dog : public Animal { public: void bark() { cout << "Woof!"; } };
class Cat : public Animal { public: void meow() { cout << "Meow!"; } };

void handleAnimal(Animal* a) {
    if (Dog* d = dynamic_cast<Dog*>(a)) {
        d->bark();
    } else if (Cat* c = dynamic_cast<Cat*>(a)) {
        c->meow();
    }
}

References

try {
    Derived& d = dynamic_cast<Derived&>(baseRef);
} catch (bad_cast& e) {
    cout << "Cast failed" << endl;
}

Best Practices

✅ Do

// 1. Use virtual destructor
class Base { public: virtual ~Base() = default; };

// 2. Use override keyword
void method() override {}

// 3. Use pure virtual for interfaces
virtual void required() = 0;

// 4. Prefer references/pointers for polymorphism
void process(Shape& shape);

❌ Don't

// 1. Call virtual functions in constructor/destructor
Base() { virtualMethod(); }  // Calls Base version!

// 2. Slice objects
Derived d;
Base b = d;  // Slices - loses Derived part

// 3. Forget virtual destructor
class Base { ~Base() {} };  // Missing virtual

Quick Reference

class Base {
public:
    virtual void method() {}      // Virtual
    virtual void pure() = 0;      // Pure virtual
    virtual ~Base() = default;    // Virtual destructor
};

class Derived : public Base {
public:
    void method() override {}     // Override
    void pure() override {}       // Must implement
    void final_m() final {}       // Cannot override
};

// Polymorphic usage
Base* ptr = new Derived();
ptr->method();                    // Calls Derived::method
delete ptr;                       // Proper cleanup

// Dynamic cast
if (auto* d = dynamic_cast<Derived*>(ptr)) {
    // Use d
}

Compile & Run

g++ -std=c++17 -Wall examples.cpp -o examples && ./examples
README - C++ Tutorial | DeepML