C++ Runtime Polymorphism: Complete Guide with Examples
C++ Runtime Polymorphism: Complete Guide with Examples
Runtime polymorphism (also called late binding or dynamic binding) allows C++ to determine which function to call at runtime based on the actual object type, not the pointer or reference type. This guide covers all aspects of runtime polymorphism with practical examples.
What is Runtime Polymorphism?
Runtime polymorphism enables a program to call the correct function based on the actual object type at runtime, even when accessed through a base class pointer or reference. This is achieved through virtual functions and vtables.
Compile-Time vs Runtime Polymorphism
#include <iostream>
class Base {
public:
// Non-virtual: compile-time binding
void compileTimeFunc() {
std::cout << "Base::compileTimeFunc()" << std::endl;
}
// Virtual: runtime binding
virtual void runtimeFunc() {
std::cout << "Base::runtimeFunc()" << std::endl;
}
};
class Derived : public Base {
public:
void compileTimeFunc() {
std::cout << "Derived::compileTimeFunc()" << std::endl;
}
void runtimeFunc() override {
std::cout << "Derived::runtimeFunc()" << std::endl;
}
};
int main() {
Derived derived;
Base* basePtr = &derived;
// Compile-time binding: based on pointer type
basePtr->compileTimeFunc(); // Output: Base::compileTimeFunc()
// Runtime binding: based on actual object type
basePtr->runtimeFunc(); // Output: Derived::runtimeFunc()
return 0;
}
How Runtime Polymorphism Works
Runtime polymorphism is implemented using virtual function tables (vtables):
- VTable Creation: Each class with virtual functions has a vtable
- VTable Pointer: Each object contains a pointer to its class’s vtable
- Function Lookup: Virtual function calls go through the vtable
- Runtime Resolution: The correct function is determined at runtime
VTable Mechanism
class Base {
public:
virtual void func1() { }
virtual void func2() { }
int data;
};
class Derived : public Base {
public:
void func1() override { }
virtual void func3() { }
int moreData;
};
Memory Layout:
Base object:
[VTable pointer] -> points to Base vtable
[data: int]
Base vtable:
[0] -> Base::func1()
[1] -> Base::func2()
Derived object:
[VTable pointer] -> points to Derived vtable
[data: int] (from Base)
[moreData: int]
Derived vtable:
[0] -> Derived::func1() (overridden)
[1] -> Base::func2() (inherited)
[2] -> Derived::func3() (new)
Virtual Functions and Runtime Polymorphism
Virtual functions are the mechanism that enables runtime polymorphism:
#include <iostream>
#include <vector>
class Animal {
public:
virtual void makeSound() {
std::cout << "Some animal sound" << std::endl;
}
virtual void move() {
std::cout << "Animal moves" << std::endl;
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Woof! Woof!" << std::endl;
}
void move() override {
std::cout << "Dog runs on four legs" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Meow! Meow!" << std::endl;
}
void move() override {
std::cout << "Cat walks gracefully" << std::endl;
}
};
class Bird : public Animal {
public:
void makeSound() override {
std::cout << "Chirp! Chirp!" << std::endl;
}
void move() override {
std::cout << "Bird flies" << std::endl;
}
};
int main() {
std::vector<Animal*> animals;
animals.push_back(new Dog());
animals.push_back(new Cat());
animals.push_back(new Bird());
animals.push_back(new Dog());
// Runtime polymorphism: correct function called for each object
for (Animal* animal : animals) {
animal->makeSound(); // Calls appropriate derived class function
animal->move();
std::cout << "---" << std::endl;
}
// Cleanup
for (Animal* animal : animals) {
delete animal;
}
return 0;
}
Output:
Woof! Woof!
Dog runs on four legs
---
Meow! Meow!
Cat walks gracefully
---
Chirp! Chirp!
Bird flies
---
Woof! Woof!
Dog runs on four legs
---
Practical Examples
Example 1: Shape Hierarchy
#include <iostream>
#include <vector>
#include <cmath>
#include <memory>
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual void draw() const = 0;
virtual std::string getName() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) { }
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
void draw() const override {
std::cout << "Drawing circle (radius: " << radius << ")" << std::endl;
}
std::string getName() const override {
return "Circle";
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) { }
double area() const override {
return width * height;
}
double perimeter() const override {
return 2 * (width + height);
}
void draw() const override {
std::cout << "Drawing rectangle (" << width << "x" << height << ")" << std::endl;
}
std::string getName() const override {
return "Rectangle";
}
};
class Triangle : public Shape {
private:
double a, b, c;
public:
Triangle(double side1, double side2, double side3)
: a(side1), b(side2), c(side3) { }
double area() const override {
// Heron's formula
double s = (a + b + c) / 2.0;
return std::sqrt(s * (s - a) * (s - b) * (s - c));
}
double perimeter() const override {
return a + b + c;
}
void draw() const override {
std::cout << "Drawing triangle (sides: " << a << ", " << b << ", " << c << ")" << std::endl;
}
std::string getName() const override {
return "Triangle";
}
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(std::make_unique<Triangle>(3.0, 4.0, 5.0));
shapes.push_back(std::make_unique<Circle>(3.0));
// Runtime polymorphism: each shape behaves according to its type
for (const auto& shape : shapes) {
std::cout << "Shape: " << shape->getName() << std::endl;
shape->draw();
std::cout << "Area: " << shape->area() << std::endl;
std::cout << "Perimeter: " << shape->perimeter() << std::endl;
std::cout << "---" << std::endl;
}
return 0;
}
Example 2: Employee Management System
#include <iostream>
#include <vector>
#include <string>
#include <memory>
class Employee {
protected:
std::string name;
double baseSalary;
public:
Employee(const std::string& n, double salary)
: name(n), baseSalary(salary) { }
virtual double calculateSalary() const {
return baseSalary;
}
virtual void work() const = 0;
virtual std::string getRole() const = 0;
virtual ~Employee() = default;
std::string getName() const {
return name;
}
};
class Developer : public Employee {
public:
Developer(const std::string& n, double salary)
: Employee(n, salary) { }
double calculateSalary() const override {
return baseSalary * 1.2; // 20% bonus
}
void work() const override {
std::cout << name << " is writing code" << std::endl;
}
std::string getRole() const override {
return "Developer";
}
};
class Manager : public Employee {
private:
int teamSize;
public:
Manager(const std::string& n, double salary, int team)
: Employee(n, salary), teamSize(team) { }
double calculateSalary() const override {
return baseSalary * 1.5 + teamSize * 1000; // Bonus + team bonus
}
void work() const override {
std::cout << name << " is managing a team of " << teamSize << " people" << std::endl;
}
std::string getRole() const override {
return "Manager";
}
};
class Designer : public Employee {
public:
Designer(const std::string& n, double salary)
: Employee(n, salary) { }
double calculateSalary() const override {
return baseSalary * 1.15; // 15% bonus
}
void work() const override {
std::cout << name << " is designing user interfaces" << std::endl;
}
std::string getRole() const override {
return "Designer";
}
};
int main() {
std::vector<std::unique_ptr<Employee>> employees;
employees.push_back(std::make_unique<Developer>("Alice", 80000));
employees.push_back(std::make_unique<Manager>("Bob", 100000, 5));
employees.push_back(std::make_unique<Designer>("Charlie", 70000));
employees.push_back(std::make_unique<Developer>("David", 85000));
// Runtime polymorphism: each employee's methods called correctly
for (const auto& emp : employees) {
std::cout << emp->getName() << " (" << emp->getRole() << ")" << std::endl;
emp->work();
std::cout << "Salary: $" << emp->calculateSalary() << std::endl;
std::cout << "---" << std::endl;
}
return 0;
}
Example 3: Payment Processing System
#include <iostream>
#include <vector>
#include <string>
#include <memory>
class PaymentMethod {
public:
virtual bool processPayment(double amount) = 0;
virtual std::string getMethodName() const = 0;
virtual ~PaymentMethod() = default;
};
class CreditCard : public PaymentMethod {
private:
std::string cardNumber;
public:
CreditCard(const std::string& card) : cardNumber(card) { }
bool processPayment(double amount) override {
std::cout << "Processing credit card payment of $" << amount
<< " with card ending in " << cardNumber.substr(cardNumber.length() - 4)
<< std::endl;
return true;
}
std::string getMethodName() const override {
return "Credit Card";
}
};
class PayPal : public PaymentMethod {
private:
std::string email;
public:
PayPal(const std::string& e) : email(e) { }
bool processPayment(double amount) override {
std::cout << "Processing PayPal payment of $" << amount
<< " for " << email << std::endl;
return true;
}
std::string getMethodName() const override {
return "PayPal";
}
};
class BankTransfer : public PaymentMethod {
private:
std::string accountNumber;
public:
BankTransfer(const std::string& account) : accountNumber(account) { }
bool processPayment(double amount) override {
std::cout << "Processing bank transfer of $" << amount
<< " to account " << accountNumber << std::endl;
return true;
}
std::string getMethodName() const override {
return "Bank Transfer";
}
};
class PaymentProcessor {
public:
static void processPayments(const std::vector<std::unique_ptr<PaymentMethod>>& methods,
double amount) {
for (const auto& method : methods) {
std::cout << "Using " << method->getMethodName() << ":" << std::endl;
method->processPayment(amount);
std::cout << "---" << std::endl;
}
}
};
int main() {
std::vector<std::unique_ptr<PaymentMethod>> paymentMethods;
paymentMethods.push_back(std::make_unique<CreditCard>("1234567890123456"));
paymentMethods.push_back(std::make_unique<PayPal>("user@example.com"));
paymentMethods.push_back(std::make_unique<BankTransfer>("ACC123456789"));
// Runtime polymorphism: each payment method processes correctly
PaymentProcessor::processPayments(paymentMethods, 100.0);
return 0;
}
Runtime Polymorphism Requirements
For runtime polymorphism to work:
- Base class function must be virtual
- Derived class function must override (same signature)
- Access through pointer or reference to base class
- Virtual destructor in base class for proper cleanup
Complete Example
class Base {
public:
virtual void func() { } // ✅ Virtual
virtual ~Base() { } // ✅ Virtual destructor
};
class Derived : public Base {
public:
void func() override { } // ✅ Overrides
~Derived() override { } // ✅ Destructor
};
int main() {
Base* ptr = new Derived(); // ✅ Base pointer
ptr->func(); // ✅ Runtime polymorphism works
delete ptr; // ✅ Proper cleanup
return 0;
}
Performance Considerations
Runtime polymorphism has some overhead:
- VTable lookup: One indirection per virtual function call
- Memory overhead: VTable pointer per object (typically 8 bytes)
- Cache effects: VTable lookups can cause cache misses
- Inlining: Virtual functions generally cannot be inlined
When to Use Runtime Polymorphism
✅ Use when:
- You need different behavior for different derived types
- You’re using base class pointers/references
- Flexibility and extensibility are important
❌ Consider alternatives when:
- Performance is critical
- Template-based polymorphism (CRTP) fits better
- Compile-time polymorphism is sufficient
Best Practices
- Use virtual functions for runtime polymorphism
- Always use virtual destructors in polymorphic base classes
- Use
overridekeyword (C++11+) for clarity and safety - Use smart pointers for automatic memory management
- Document virtual functions that are meant to be overridden
- Consider performance when polymorphism is in hot paths
- Use pure virtual functions for abstract interfaces
Summary
Runtime polymorphism is a powerful feature of C++:
- Enables late binding through virtual functions and vtables
- Allows flexible, extensible code through base class interfaces
- Requires virtual functions and proper inheritance
- Has performance overhead but provides flexibility
- Essential for object-oriented design patterns
- Use with virtual destructors for proper cleanup
Understanding runtime polymorphism is crucial for effective C++ object-oriented programming.