Polymorphism in C++

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:
| Type | Also Called | Dispatch | When it Happens |
| Compile-time Polymorphism | Static polymorphism | Function overloading, operator overloading, templates | Compile-time |
| Run-time Polymorphism | Dynamic polymorphism | Virtual functions | Run-time |
2. Why Polymorphism? A Real Example
Imagine you’re building a graphics engine.
You may have:
LongRangeCameraFisheyeCameraOrthographicCamera
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:
Read the object’s vptr
Follow it to the vtable
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
Circleruns firstDestructor of
Shaperuns 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




