Skip to content

Polymorphism

Polymorphism is one of the four pillars of OOP, meaning “many forms.” It allows objects of different classes to be treated as objects of a common superclass. This chapter covers how to achieve polymorphism in C++ using virtual functions.

┌─────────────────────────────────────────────────────────────┐
│ Types of Polymorphism │
├─────────────────────────────────────────────────────────────┤
│ │
│ Compile-time (Static) Runtime (Dynamic) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 1. Function │ │ 1. Virtual │ │
│ │ Overloading │ │ Functions │ │
│ │ │ │ │ │
│ │ 2. Operator │ │ 2. Inheritance │ │
│ │ Overloading │ │ (Subtyping) │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

A virtual function is a member function that can be overridden in derived classes:

class Animal {
public:
// Virtual function - can be overridden
virtual void speak() {
std::cout << "Animal speaks\n";
}
// Virtual destructor (important!)
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() override { // Overrides Animal::speak
std::cout << "Dog barks: Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Cat meows: Meow!\n";
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->speak(); // Calls Dog::speak
animal2->speak(); // Calls Cat::speak
delete animal1;
delete animal2;
}

Behind the scenes, virtual functions use a virtual table (vtable):

┌─────────────────────────────────────────────────────────────┐
│ Virtual Table (vtable) │
├─────────────────────────────────────────────────────────────┤
│ │
│ Animal Object Dog Object Cat Object │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ vptr ───────┼───────│ vptr ───────┼─────│ vptr ───────│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Animal:: │ │ Dog:: │ │ Cat:: │ │
│ │ speak() │ │ speak() │ │ speak() │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

A pure virtual function has no implementation in the base class:

class Shape {
public:
// Pure virtual = abstract method
virtual double area() const = 0;
virtual void draw() const = 0;
virtual ~Shape() {}
};
// Cannot create Shape objects - it's abstract
// Shape s; // Error!
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
std::cout << "Drawing circle\n";
}
};

Abstract classes cannot be instantiated - they’re just blueprints.

Use override to ensure you’re actually overriding a virtual function:

class Base {
public:
virtual void func() {}
};
class Derived : public Base {
public:
void func() override {} // Correct
// void func(int) override {} // Error! Not overriding anything
// void notVirtual() override {} // Error! Not virtual
};
class Animal {
public:
virtual void speak() {
std::cout << "Some animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!\n";
// Can still call base implementation
Animal::speak();
}
};

Use dynamic_cast for safe downcasting:

class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
void specialMethod() {
std::cout << "Special!\n";
}
};
int main() {
Base* base = new Derived();
// Unsafe cast (don't do this)
// Derived* d = static_cast<Derived*>(base);
// Safe cast
Derived* d = dynamic_cast<Derived*>(base);
if (d) {
d->specialMethod();
}
delete base;
}

You can override with different access levels:

class Base {
public:
virtual void method() {}
};
class Derived : public Base {
private:
void method() override {} // Now private in Derived!
};
int main() {
Derived d;
// d.method(); // Error! method is private
Base* ptr = &d;
ptr->method(); // OK! Calls Derived::method
}

Virtual Functions in Constructors/Destructors

Section titled “Virtual Functions in Constructors/Destructors”

Be careful with virtual functions in constructors and destructors:

class Base {
public:
Base() {
// Virtual functions don't work as expected here!
// Calls Base::method(), not Derived::method()
method();
}
virtual void method() {
std::cout << "Base\n";
}
virtual ~Base() {
// Similarly, destructor calls base version
method();
}
};
class Derived : public Base {
public:
void method() override {
std::cout << "Derived\n";
}
};

Multiple Inheritance and Virtual Functions

Section titled “Multiple Inheritance and Virtual Functions”
class Printable {
public:
virtual void print() const = 0;
virtual ~Printable() {}
};
class Logger {
public:
virtual void log() const = 0;
virtual ~Logger() {}
};
class Document : public Printable, public Logger {
public:
void print() const override {
std::cout << "Printing\n";
}
void log() const override {
std::cout << "Logging\n";
}
};
#include <iostream>
#include <vector>
#include <memory>
#include <string>
class PaymentMethod {
protected:
std::string holderName;
public:
PaymentMethod(const std::string& holder) : holderName(holder) {}
virtual bool processPayment(double amount) = 0;
virtual ~PaymentMethod() {}
};
class CreditCard : public PaymentMethod {
private:
std::string cardNumber;
double creditLimit;
public:
CreditCard(const std::string& holder, const std::string& num, double limit)
: PaymentMethod(holder), cardNumber(num), creditLimit(limit) {}
bool processPayment(double amount) override {
if (amount > creditLimit) {
std::cout << "Credit limit exceeded\n";
return false;
}
creditLimit -= amount;
std::cout << "Credit card payment of $" << amount
<< " processed for " << holderName << "\n";
return true;
}
};
class PayPal : public PaymentMethod {
private:
std::string email;
public:
PayPal(const std::string& holder, const std::string& em)
: PaymentMethod(holder), email(em) {}
bool processPayment(double amount) override {
std::cout << "PayPal payment of $" << amount
<< " processed for " << holderName << "\n";
return true;
}
};
class BankTransfer : public PaymentMethod {
private:
std::string accountNumber;
public:
BankTransfer(const std::string& holder, const std::string& acc)
: PaymentMethod(holder), accountNumber(acc) {}
bool processPayment(double amount) override {
std::cout << "Bank transfer of $" << amount
<< " processed for " << holderName << "\n";
return true;
}
};
class ShoppingCart {
private:
std::vector<std::pair<std::string, double>> items;
std::unique_ptr<PaymentMethod> paymentMethod;
public:
void addItem(const std::string& name, double price) {
items.emplace_back(name, price);
}
void setPaymentMethod(std::unique_ptr<PaymentMethod> method) {
paymentMethod = std::move(method);
}
bool checkout() {
double total = 0;
for (const auto& item : items) {
total += item.second;
}
std::cout << "Total: $" << total << "\n";
if (paymentMethod) {
return paymentMethod->processPayment(total);
}
std::cout << "No payment method set\n";
return false;
}
};
int main() {
ShoppingCart cart;
cart.addItem("Laptop", 999.99);
cart.addItem("Mouse", 29.99);
// Using different payment methods polymorphically
cart.setPaymentMethod(std::make_unique<CreditCard>(
"John Doe", "4111111111111111", 1000));
cart.checkout();
std::cout << "\n";
cart.setPaymentMethod(std::make_unique<PayPal>(
"John Doe", "john@example.com"));
cart.checkout();
return 0;
}
  • Polymorphism allows treating different types uniformly
  • Virtual functions enable runtime polymorphism through the vtable
  • Use virtual to mark functions as overridable
  • Use = 0 for pure virtual functions (abstract methods)
  • Use override keyword to catch errors
  • Always make base class destructors virtual when using polymorphism
  • Abstract classes cannot be instantiated but define interfaces

Now let’s learn about encapsulation and access specifiers.

Next Chapter: 12_encapsulation.md - Encapsulation and Access Specifiers