Skip to main content

Command Palette

Search for a command to run...

Polymorphism in C++

Updated
5 min read
Polymorphism in C++
D

I'm an Engineer

Polymorphism is one of the most powerful ideas in object oriented programming. It allows the same function call to behave differently depending on the underlying object. In C++, polymorphism concept forms the backbone of system desgin, plugin architecture, driver interfaces and any large scale extensible software development.

In this article, we’ll explore what polymorphism is, how C++ implements it, how vtables work, when to use dynamic vs static polymorphism, common pitfalls, compiler optimizations, and advanced tricks used in production-grade systems (including embedded and performance-critical codebases).


1. What is Polymorphism?

Polymorphism = “many forms”.
The ability of a single interface to represent different underlying data types or behaviors.

In C++, polymorphism comes in two forms:

TypeAlso CalledDispatchWhen it Happens
Compile-time PolymorphismStatic polymorphismFunction overloading, operator overloading, templatesCompile-time
Run-time PolymorphismDynamic polymorphismVirtual functionsRun-time

2. Why Polymorphism? A Real Example

Imagine you’re building a graphics engine.
You may have:

  • LongRangeCamera

  • FisheyeCamera

  • OrthographicCamera

Each needs to implement a function:

void capture_frame();

But each camera behaves differently. Instead of writing:

capture_longrangeCam();
capture_fisheyeCam();
capture_orthoCam();

We want:

Camera* cam = new FisheyeCamera();
cam->capture_frame();   // behaves differently depending on instance

This is polymorphism.


3. How Run-Time Polymorphism Works in C++

Run-time polymorphism in C++ is powered by virtual functions.
Example:

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing Shape\n";
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Circle\n";
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Rectangle\n";
    }
};

Using it:

Shape* s1 = new Circle();
Shape* s2 = new Rectangle();

s1->draw();   // Drawing Circle
s2->draw();   // Drawing Rectangle

4. What Actually Happens Internally?

When a class contains a virtual function:

  • The compiler generates a virtual table (vtable)

  • Each object contains a pointer to its class's vtable (called vptr)


4.1 Memory Layout Visualization

Shape object:

 --------------------
| vptr ---> [vtable] |
|--------------------|
|  Shape data...     |
 --------------------

Circle object:

 --------------------
| vptr ---> [vtable] --> draw() = Circle::draw
|--------------------|
|  Circle data...    |
 --------------------

4.2 VTable Diagram

          Shape VTable                         Circle VTable
    +-----------------------+             +-----------------------+
    | Shape::draw() ------- |             | Circle::draw() -------|
    +-----------------------+             +-----------------------+
           ^                                         ^
           | (vptr)                                  | (vptr)
       +---------+                               +----------+
       |  Shape  |                               |  Circle  |
       +---------+                               +----------+

When you call:

s1->draw();

C++ performs:

  1. Read the object’s vptr

  2. Follow it to the vtable

  3. Jump to the correct override function

This is dynamic dispatch.


5. Virtual Destructor — Why It is Critical

If a base class has any virtual functions, it must have a virtual destructor.

Wrong (memory leak):

Shape* s = new Circle();
delete s;   // undefined behavior

Correct:

class Shape {
public:
    virtual ~Shape() = default;
};

This ensures:

  • Destructor of Circle runs first

  • Destructor of Shape runs next


6. Pure Virtual Functions & Abstract Classes

Pure virtual function:

virtual void draw() = 0;

Abstract class:

class Shape {
public:
    virtual void draw() = 0;    // no definition
};

You cannot instantiate an abstract class:

Shape s;  // ERROR

Benefits:

  • Provides an interface

  • Derived classes must implement functionality


7. Complete Working Example

#include <iostream>
#include <memory>

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

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Circle\n";
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing Rectangle\n";
    }
};

void Render(Shape& shape) {
    shape.draw();
}

int main() {
    std::unique_ptr<Shape> s1 = std::make_unique<Circle>();
    std::unique_ptr<Shape> s2 = std::make_unique<Rectangle>();

    Render(*s1);
    Render(*s2);
}

Output:

Drawing Circle
Drawing Rectangle

8. Compile-Time Polymorphism (Static Polymorphism)

Compile-time polymorphism is resolved entirely by the compiler. This means:

  • No vtable overhead

  • No runtime cost

  • Often leads to inlined, extremely fast code

  • Errors show up during compilation, not execution

This is the foundation of modern high-performance C++ libraries like STL, Eigen, Boost, CUDA Thrust, and Intel TBB.


8.1 Function Overloading

Multiple functions with the same name but different parameters.

void Log(int value)      { std::cout << "int\n"; }
void Log(double value)   { std::cout << "double\n"; }
void Log(std::string s)  { std::cout << "string\n"; }

Log(10);        // calls Log(int)
Log(3.14);      // calls Log(double)
Log("hello");   // calls Log(string)

8.2 Operator Overloading

class Vector {
public:
    float x, y;

    Vector operator+(const Vector& other) const {
        return {x + other.x, y + other.y};
    }
};

Compile-time dispatch ensures extremely fast math operations.


8.3 Template Polymorphism

Templates are the purest form of compile-time polymorphism.

template<typename T>
T Add(T a, T b) {
    return a + b;
}

Add<int>(1, 2);       // creates Add<int>
Add<double>(1, 2.5);  // creates Add<double>

The compiler generates different functions for each type → known as template instantiation.


8.4 CRTP (Curiously Recurring Template Pattern)

Used for zero-cost polymorphism—especially in libraries requiring extreme performance.

template<typename Derived>
class Shape {
public:
    void Draw() {
        static_cast<Derived*>(this)->DrawImpl();
    }
};

class Circle : public Shape<Circle> {
public:
    void DrawImpl() {
        std::cout << "Drawing Circle\n";
    }
};
  • No virtual functions

  • No vtable

  • All resolved at compile time

  • Often gets inlined → zero overhead


Summary

Compile-Time Polymorphism

  • Templates

  • Function overloading

  • Operator overloading

  • CRTP

  • Resolved at compile time

  • Zero overhead

Run-Time Polymorphism

  • Virtual functions

  • vtables + vptr

  • Late binding

  • Dynamic, flexible


30 views

More from this blog

The Engineering Hub

11 posts

knowledge sharing portal...