Runtime polymorphism, also known as dynamic polymorphism or late binding, is a type of polymorphism that is resolved during program execution rather than at compile time. In C++, runtime polymorphism is achieved through virtual functions and function overriding.
What is Runtime Polymorphism?
Runtime polymorphism occurs when the decision about which function to call is made during program execution based on the actual type of the object, not its declared type. This is different from compile time polymorphism, where the decision is made by the compiler before the program runs.
The key characteristic of runtime polymorphism is that the binding of function calls to their definitions happens at runtime based on the actual object type being referenced through a base class pointer or reference.
Virtual Functions and Function Overriding
Virtual functions are the primary mechanism for implementing runtime polymorphism in C++. A virtual function is a member function that is declared in a base class and redefined (overridden) in derived classes.
Example of Runtime Polymorphism:
#include <iostream>
using namespace std;
// Base class
class Animal {
public:
virtual void makeSound() {
cout << "Animal makes a sound" << endl;
}
};
// Derived class 1
class Dog : public Animal {
public:
void makeSound() override {
cout << "Dog barks: Woof! Woof!" << endl;
}
};
// Derived class 2
class Cat : public Animal {
public:
void makeSound() override {
cout << "Cat meows: Meow! Meow!" << endl;
}
};
// Derived class 3
class Duck : public Animal {
public:
void makeSound() override {
cout << "Duck quacks: Quack! Quack!" << endl;
}
};
int main() {
// Create objects of different types
Animal* animals[4];
animals[0] = new Animal();
animals[1] = new Dog();
animals[2] = new Cat();
animals[3] = new Duck();
// Call makeSound() for each object
for (int i = 0; i < 4; i++) {
animals[i]->makeSound(); // Runtime polymorphism
}
// Clean up memory
for (int i = 0; i < 4; i++) {
delete animals[i];
}
return 0;
}
Output:
Animal makes a sound
Dog barks: Woof! Woof!
Cat meows: Meow! Meow!
Duck quacks: Quack! Quack!
In this example, makeSound() is a virtual function defined in the base class Animal and overridden in each derived class. When we call makeSound() through a base class pointer, the actual function that gets called depends on the type of the object being pointed to, which is determined at runtime.
How Runtime Polymorphism Works
Runtime polymorphism in C++ is implemented using a mechanism called the virtual function table (vtable):
- For each class with virtual functions, the compiler creates a vtable.
- Each object of such a class contains a hidden pointer (vptr) that points to this vtable.
- When a virtual function is called through a base class pointer, the program:
- Accesses the object’s vtable through the vptr
- Looks up the appropriate function pointer in the vtable
- Calls the function through that pointer
This mechanism allows the program to determine which function to call at runtime based on the actual object type.
Virtual Function Tables (vtables)

As shown in the illustration, each class with virtual functions has its own vtable, and each object contains a pointer to the vtable of its class. When a virtual function is called through a base class pointer, the program uses the vtable to find the correct function to call.
Pure Virtual Functions and Abstract Classes
A pure virtual function is a virtual function that has no implementation in the base class and is declared by assigning 0:
virtual return_type function_name(parameters) = 0;
A class containing at least one pure virtual function is called an abstract class. Abstract classes cannot be instantiated, and derived classes must implement all pure virtual functions to be instantiable.
Example of Pure Virtual Functions and Abstract Classes:
#include <iostream>
using namespace std;
// Abstract base class
class Shape {
public:
// Pure virtual functions
virtual double area() = 0;
virtual double perimeter() = 0;
virtual void draw() = 0;
// Normal method that uses virtual functions
void printInfo() {
cout << "Area: " << area() << endl;
cout << "Perimeter: " << perimeter() << endl;
cout << "Drawing: ";
draw();
}
// Virtual destructor
virtual ~Shape() {}
};
// Concrete derived class
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
double perimeter() override {
return 2 * 3.14159 * radius;
}
void draw() override {
cout << "Circle with radius " << radius << endl;
}
};
// Another concrete derived class
class Rectangle : public Shape {
private:
double length;
double width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
double area() override {
return length * width;
}
double perimeter() override {
return 2 * (length + width);
}
void draw() override {
cout << "Rectangle with length " << length << " and width " << width << endl;
}
};
int main() {
// Shape shape; // Error: Cannot instantiate an abstract class
Shape* shapes[2];
shapes[0] = new Circle(5);
shapes[1] = new Rectangle(4, 3);
for (int i = 0; i < 2; i++) {
shapes[i]->printInfo();
cout << "------------------------" << endl;
}
// Clean up memory
for (int i = 0; i < 2; i++) {
delete shapes[i];
}
return 0;
}
Output:
Area: 78.5397
Perimeter: 31.4159
Drawing: Circle with radius 5
------------------------
Area: 12
Perimeter: 14
Drawing: Rectangle with length 4 and width 3
------------------------
In this example, Shape is an abstract class with three pure virtual functions: area(), perimeter(), and draw(). The derived classes Circle and Rectangle must implement these functions to be instantiable.
Virtual Destructors
When using polymorphism, it’s important to declare destructors as virtual. If a base class destructor is not virtual and an object of a derived class is deleted through a base class pointer, only the base class destructor will be called, potentially leading to memory leaks.
Example of the Problem Without Virtual Destructors:
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base constructor" << endl;
}
// Non-virtual destructor
~Base() {
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() {
cout << "Derived constructor" << endl;
data = new int[10]; // Allocate memory
}
~Derived() {
cout << "Derived destructor" << endl;
delete[] data; // Free memory
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Only Base destructor is called!
return 0;
}
Output:
Base constructor
Derived constructor
Base destructor
Note that the Derived destructor is not called, which would lead to a memory leak.
Solution with Virtual Destructors:
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base constructor" << endl;
}
// Virtual destructor
virtual ~Base() {
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() {
cout << "Derived constructor" << endl;
data = new int[10]; // Allocate memory
}
~Derived() override {
cout << "Derived destructor" << endl;
delete[] data; // Free memory
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Now both destructors are called
return 0;
}
Output:
Base constructor
Derived constructor
Derived destructor
Base destructor
With a virtual destructor, both the derived class and base class destructors are called properly.
The override and final Keywords (C++11)
C++11 introduced two keywords that help with virtual functions:
override: Explicitly indicates that a function is meant to override a virtual function from a base class.final: Prevents further overriding of a virtual function or inheritance from a class.
Example of override and final:
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() {
cout << "Base::func1()" << endl;
}
virtual void func2() {
cout << "Base::func2()" << endl;
}
virtual void func3() {
cout << "Base::func3()" << endl;
}
};
class Derived1 : public Base {
public:
// Using override makes the compiler check if this actually overrides a base method
void func1() override {
cout << "Derived1::func1()" << endl;
}
// Using final prevents further derived classes from overriding func2
void func2() override final {
cout << "Derived1::func2()" << endl;
}
// This would cause a compiler error because there's no func4() in Base
// void func4() override { cout << "Derived1::func4()" << endl; }
};
class Derived2 : public Derived1 {
public:
// This is allowed because func1 wasn't marked as final
void func1() override {
cout << "Derived2::func1()" << endl;
}
// This would cause a compiler error because func2 was marked as final in Derived1
// void func2() override { cout << "Derived2::func2()" << endl; }
void func3() override {
cout << "Derived2::func3()" << endl;
}
};
int main() {
Base* b1 = new Derived1();
Base* b2 = new Derived2();
b1->func1(); // Calls Derived1::func1()
b1->func2(); // Calls Derived1::func2()
b2->func1(); // Calls Derived2::func1()
b2->func2(); // Calls Derived1::func2() (because it's final)
b2->func3(); // Calls Derived2::func3()
delete b1;
delete b2;
return 0;
}
Output:
Derived1::func1()
Derived1::func2()
Derived2::func1()
Derived1::func2()
Derived2::func3()
Using override helps catch errors at compile time when you’re trying to override a function that doesn’t exist or has a different signature. Using final allows you to prevent further overriding of functions that shouldn’t be changed in derived classes.
Dynamic Casting
Dynamic casting is a runtime cast that checks if the conversion is valid. It’s often used with polymorphic types to safely downcast from a base class pointer to a derived class pointer.
Example of Dynamic Casting:
#include <iostream>
using namespace std;
class Base {
public:
virtual void print() {
cout << "Base class" << endl;
}
virtual ~Base() {}
};
class Derived1 : public Base {
public:
void print() override {
cout << "Derived1 class" << endl;
}
void derived1Method() {
cout << "Method specific to Derived1" << endl;
}
};
class Derived2 : public Base {
public:
void print() override {
cout << "Derived2 class" << endl;
}
void derived2Method() {
cout << "Method specific to Derived2" << endl;
}
};
int main() {
Base* basePtr1 = new Derived1();
Base* basePtr2 = new Derived2();
// Try to cast to Derived1
Derived1* d1Ptr1 = dynamic_cast<Derived1*>(basePtr1);
Derived1* d1Ptr2 = dynamic_cast<Derived1*>(basePtr2);
if (d1Ptr1) {
cout << "Successfully cast basePtr1 to Derived1*" << endl;
d1Ptr1->derived1Method();
} else {
cout << "Failed to cast basePtr1 to Derived1*" << endl;
}
if (d1Ptr2) {
cout << "Successfully cast basePtr2 to Derived1*" << endl;
d1Ptr2->derived1Method();
} else {
cout << "Failed to cast basePtr2 to Derived1*" << endl;
}
// Clean up memory
delete basePtr1;
delete basePtr2;
return 0;
}
Output:
Successfully cast basePtr1 to Derived1*
Method specific to Derived1
Failed to cast basePtr2 to Derived1*
In this example, we use dynamic_cast to safely attempt to convert base class pointers to derived class pointers. The cast succeeds only when the object is actually of the target type or a type derived from it.
Real-World Example: Graphics System
Here’s a practical example of runtime polymorphism in a simple graphics system:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// Base class for all graphical elements
class GraphicObject {
protected:
int x, y;
string color;
public:
GraphicObject(int xPos, int yPos, const string& col) :
x(xPos), y(yPos), color(col) {}
// Virtual functions for common operations
virtual void draw() = 0;
virtual void move(int newX, int newY) {
x = newX;
y = newY;
cout << "Moved to position (" << x << ", " << y << ")" << endl;
}
virtual void resize(double factor) = 0;
// Virtual destructor
virtual ~GraphicObject() {}
};
// Derived class: Circle
class Circle : public GraphicObject {
private:
double radius;
public:
Circle(int xPos, int yPos, const string& col, double r) :
GraphicObject(xPos, yPos, col), radius(r) {}
void draw() override {
cout << "Drawing a " << color << " circle at (" << x << ", " << y << ") "
<< "with radius " << radius << endl;
}
void resize(double factor) override {
radius *= factor;
cout << "Circle resized to radius " << radius << endl;
}
};
// Derived class: Rectangle
class Rectangle : public GraphicObject {
private:
double width, height;
public:
Rectangle(int xPos, int yPos, const string& col, double w, double h) :
GraphicObject(xPos, yPos, col), width(w), height(h) {}
void draw() override {
cout << "Drawing a " << color << " rectangle at (" << x << ", " << y << ") "
<< "with dimensions " << width << "x" << height << endl;
}
void resize(double factor) override {
width *= factor;
height *= factor;
cout << "Rectangle resized to dimensions " << width << "x" << height << endl;
}
};
// Derived class: Text
class Text : public GraphicObject {
private:
string content;
int fontSize;
public:
Text(int xPos, int yPos, const string& col, const string& text, int size) :
GraphicObject(xPos, yPos, col), content(text), fontSize(size) {}
void draw() override {
cout << "Drawing " << color << " text \"" << content << "\" at (" << x << ", " << y << ") "
<< "with font size " << fontSize << endl;
}
void resize(double factor) override {
fontSize = static_cast<int>(fontSize * factor);
cout << "Text resized to font size " << fontSize << endl;
}
// Additional method specific to Text
void setText(const string& newText) {
content = newText;
cout << "Text content changed to \"" << content << "\"" << endl;
}
};
// Canvas class that contains and manages graphic objects
class Canvas {
private:
vector<GraphicObject*> objects;
public:
void addObject(GraphicObject* obj) {
objects.push_back(obj);
}
void drawAll() {
cout << "\nDrawing all objects:" << endl;
for (auto obj : objects) {
obj->draw();
}
}
void resizeAll(double factor) {
cout << "\nResizing all objects by factor " << factor << ":" << endl;
for (auto obj : objects) {
obj->resize(factor);
}
}
// Clean up all objects
~Canvas() {
for (auto obj : objects) {
delete obj;
}
}
};
int main() {
Canvas canvas;
// Add different types of graphic objects
canvas.addObject(new Circle(100, 100, "red", 50));
canvas.addObject(new Rectangle(200, 200, "blue", 80, 40));
canvas.addObject(new Text(300, 300, "green", "Hello, World!", 12));
// Draw all objects
canvas.drawAll();
// Resize all objects
canvas.resizeAll(1.5);
// Draw all objects after resizing
canvas.drawAll();
// Demonstrate casting and calling derived class specific method
GraphicObject* obj = new Text(400, 400, "purple", "Initial text", 14);
obj->draw();
// Use dynamic_cast to safely downcast to Text*
Text* textObj = dynamic_cast<Text*>(obj);
if (textObj) {
textObj->setText("Modified text");
textObj->draw();
}
delete obj;
return 0;
}
Output:
Drawing all objects:
Drawing a red circle at (100, 100) with radius 50
Drawing a blue rectangle at (200, 200) with dimensions 80x40
Drawing green text "Hello, World!" at (300, 300) with font size 12
Resizing all objects by factor 1.5:
Circle resized to radius 75
Rectangle resized to dimensions 120x60
Text resized to font size 18
Drawing all objects:
Drawing a red circle at (100, 100) with radius 75
Drawing a blue rectangle at (200, 200) with dimensions 120x60
Drawing green text "Hello, World!" at (300, 300) with font size 18
Drawing purple text "Initial text" at (400, 400) with font size 14
Text content changed to "Modified text"
Drawing purple text "Modified text" at (400, 400) with font size 14
In this example, we have a graphics system with a base GraphicObject class and three derived classes: Circle, Rectangle, and Text. The Canvas class manages a collection of graphic objects through base class pointers, demonstrating runtime polymorphism when calling draw() and resize() on each object.
Common Use Cases for Runtime Polymorphism
- User Interface Frameworks: Different UI elements share common behaviors but have unique implementations.
- Game Development: Various game entities (characters, items, obstacles) that need to be updated and rendered.
- Plugin Systems: Base interfaces that plugins must implement to integrate with a system.
- Device Drivers: Common interfaces for different hardware implementations.
- File Systems: Different file types with common operations like open, read, write, and close.
Advantages of Runtime Polymorphism
- Flexibility: Behavior can change at runtime based on the actual object type.
- Extensibility: New derived classes can be added without modifying existing code.
- Maintainability: Common interfaces make code easier to understand and maintain.
- Code Reuse: Base class code can be reused by multiple derived classes.
- Abstraction: Allows interaction with objects based on their abstract behavior rather than concrete implementation.
Disadvantages of Runtime Polymorphism
- Performance Overhead: Virtual function calls have a small performance cost due to vtable lookup.
- Memory Overhead: Each object with virtual functions needs extra memory for the vptr.
- Complexity: Dynamic binding can make code flow harder to follow.
- Runtime Errors: Type casting issues might not be caught until runtime.
Best Practices for Runtime Polymorphism
- Use Virtual Destructors: Always make destructors virtual in base classes to ensure proper cleanup.
- Use
overrideandfinal: These keywords help catch errors and express intent clearly. - Prefer Dynamic Cast for Downcasting: When converting base class pointers to derived class pointers, use
dynamic_castfor safety. - Keep Interface Simple: Design base classes with a clean, coherent interface that makes sense for all derived classes.
- Consider Factory Methods: Use factory methods to create objects dynamically based on runtime conditions.
Summary
Runtime polymorphism is a powerful feature in C++ that allows objects of different types to be treated through a common interface. It is achieved through:
- Virtual Functions: Base class functions that can be overridden in derived classes
- Pure Virtual Functions and Abstract Classes: Define interfaces that derived classes must implement
- Dynamic Binding: The decision about which function to call is made at runtime based on the actual object type
Runtime polymorphism enables you to write more flexible and extensible code by focusing on the abstract behaviors of objects rather than their concrete implementations. It’s an essential tool for implementing the “Open/Closed Principle” (open for extension, closed for modification) in object-oriented design.