Polymorphism
Polymorphism means "many forms" — one interface, multiple implementations. C++ supports two kinds: compile-time polymorphism (function overloading and templates, resolved by the compiler) and runtime polymorphism (virtual functions, resolved at execution time through a vtable).
Compile-Time Polymorphism
#include <iostream>
#include <cmath>
using namespace std;
// Function overloading — same name, different parameter types
double area(double r) {
return 3.14159265 * r * r; // Circle
}
double area(double w, double h) {
return w * h; // Rectangle
}
double area(double a, double b, double c) {
// Heron's formula for triangle
double s = (a + b + c) / 2;
return sqrt(s * (s-a) * (s-b) * (s-c));
}
// Template polymorphism
template <typename T>
T maxOf(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << area(5.0) << "\n"; // 78.5398 (circle)
cout << area(4.0, 6.0) << "\n"; // 24 (rectangle)
cout << area(3.0, 4.0, 5.0) << "\n"; // 6 (triangle)
cout << maxOf(10, 20) << "\n"; // 20 (int)
cout << maxOf(3.14, 2.71) << "\n"; // 3.14 (double)
cout << maxOf('a', 'z') << "\n"; // z (char)
return 0;
}Runtime Polymorphism — The Vtable
When a class has virtual functions, the compiler creates a vtable (virtual dispatch table) — an array of function pointers. Each object stores a hidden vptr (vtable pointer). When you call a virtual function through a pointer or reference, the CPU looks up the vtable at runtime:
Base* p = new Derived();
p->foo(); // Compiler generates: (*p->vptr[index_of_foo])(p)#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() const { cout << "...\n"; }
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() const override { cout << "Woof!\n"; }
};
class Cat : public Animal {
public:
void speak() const override { cout << "Meow!\n"; }
};
class Duck : public Animal {
public:
void speak() const override { cout << "Quack!\n"; }
};
void makeSpeak(const Animal& a) {
a.speak(); // Runtime dispatch — calls the right derived version
}
int main() {
Dog dog; Cat cat; Duck duck;
makeSpeak(dog); // Woof!
makeSpeak(cat); // Meow!
makeSpeak(duck); // Quack!
// Polymorphism via pointer array
Animal* animals[] = { new Dog, new Cat, new Duck };
for (Animal* a : animals) {
a->speak(); // Vtable dispatch picks the right implementation
delete a; // Virtual destructor ensures correct cleanup
}
// Woof! Meow! Quack!
return 0;
}Interface Pattern with Pure Virtual
In C++, an interface is simply an abstract class with only pure virtual functions and a virtual destructor:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
// Interface — no data, no implementation
class Serialisable {
public:
virtual string serialise() = 0;
virtual void deserialise(const string& data) = 0;
virtual ~Serialisable() {}
};
class Printable {
public:
virtual void print() const = 0;
virtual ~Printable() {}
};
// Class that implements multiple interfaces
class Config : public Serialisable, public Printable {
string host;
int port;
public:
Config(const string& h, int p) : host(h), port(p) {}
string serialise() override {
return host + ":" + to_string(port);
}
void deserialise(const string& data) override {
size_t colon = data.find(':');
host = data.substr(0, colon);
port = stoi(data.substr(colon + 1));
}
void print() const override {
cout << "Config: " << host << ":" << port << "\n";
}
};
int main() {
Config cfg("api.example.com", 443);
cfg.print(); // Config: api.example.com:443
string serialised = cfg.serialise();
cout << "Serialised: " << serialised << "\n"; // api.example.com:443
Config cfg2("", 0);
cfg2.deserialise("localhost:8080");
cfg2.print(); // Config: localhost:8080
return 0;
}dynamic_cast — Safe Downcasting
dynamic_cast checks at runtime whether a base pointer actually points to a specific derived type:
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Vehicle {
public:
virtual string type() const = 0;
virtual ~Vehicle() {}
};
class Car : public Vehicle {
public:
string type() const override { return "Car"; }
void honk() const { cout << "Beep!\n"; }
};
class Truck : public Vehicle {
public:
string type() const override { return "Truck"; }
void loadCargo() const { cout << "Loading cargo...\n"; }
};
int main() {
vector<unique_ptr<Vehicle>> fleet;
fleet.push_back(make_unique<Car>());
fleet.push_back(make_unique<Truck>());
fleet.push_back(make_unique<Car>());
for (auto& v : fleet) {
cout << v->type() << ": ";
// dynamic_cast returns nullptr if the cast fails
if (Car* car = dynamic_cast<Car*>(v.get())) {
car->honk(); // Beep!
} else if (Truck* truck = dynamic_cast<Truck*>(v.get())) {
truck->loadCargo(); // Loading cargo...
}
}
// Car: Beep!
// Truck: Loading cargo...
// Car: Beep!
return 0;
}Note: dynamic_cast requires the base class to have at least one virtual function (RTTI must be enabled). Prefer designing with virtual functions over dynamic_cast — frequent downcasting is a sign of poor design.
The Visitor Pattern
The Visitor pattern achieves double dispatch — calling the right method based on both the visitor type and the element type:
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Circle; class Rectangle; class Triangle; // Forward declarations
class ShapeVisitor {
public:
virtual void visit(Circle& c) = 0;
virtual void visit(Rectangle& r) = 0;
virtual void visit(Triangle& t) = 0;
virtual ~ShapeVisitor() {}
};
class Shape {
public:
virtual void accept(ShapeVisitor& v) = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
public:
double radius;
Circle(double r) : radius(r) {}
void accept(ShapeVisitor& v) override { v.visit(*this); }
};
class Rectangle : public Shape {
public:
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
void accept(ShapeVisitor& v) override { v.visit(*this); }
};
class Triangle : public Shape {
public:
double base, height;
Triangle(double b, double h) : base(b), height(h) {}
void accept(ShapeVisitor& v) override { v.visit(*this); }
};
class AreaCalculator : public ShapeVisitor {
public:
double total = 0;
void visit(Circle& c) override { total += 3.14159 * c.radius * c.radius; }
void visit(Rectangle& r) override { total += r.width * r.height; }
void visit(Triangle& t) override { total += 0.5 * t.base * t.height; }
};
int main() {
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(5.0));
shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(make_unique<Triangle>(3.0, 8.0));
AreaCalculator calc;
for (auto& s : shapes) s->accept(calc);
cout << "Total area: " << calc.total << "\n"; // 78.5398 + 24 + 12 = 114.54
return 0;
}Practical Example: Expression Evaluator
#include <iostream>
#include <memory>
using namespace std;
// Abstract Expression node
class Expr {
public:
virtual double eval() const = 0;
virtual string str() const = 0;
virtual ~Expr() {}
};
class Number : public Expr {
double val;
public:
Number(double v) : val(v) {}
double eval() const override { return val; }
string str() const override { return to_string(val); }
};
class BinaryOp : public Expr {
char op;
unique_ptr<Expr> left, right;
public:
BinaryOp(char op, unique_ptr<Expr> l, unique_ptr<Expr> r)
: op(op), left(move(l)), right(move(r)) {}
double eval() const override {
switch (op) {
case '+': return left->eval() + right->eval();
case '-': return left->eval() - right->eval();
case '*': return left->eval() * right->eval();
case '/': return left->eval() / right->eval();
}
return 0;
}
string str() const override {
return "(" + left->str() + " " + op + " " + right->str() + ")";
}
};
int main() {
// Build AST for: (3 + 4) * (10 - 2)
auto expr = make_unique<BinaryOp>('*',
make_unique<BinaryOp>('+', make_unique<Number>(3), make_unique<Number>(4)),
make_unique<BinaryOp>('-', make_unique<Number>(10), make_unique<Number>(2))
);
cout << expr->str() << "\n"; // ((3 + 4) * (10 - 2))
cout << expr->eval() << "\n"; // 56
return 0;
}Summary
- Compile-time polymorphism — function overloading and templates — is resolved by the compiler; zero runtime cost
- Runtime polymorphism — virtual functions — uses a vtable to dispatch calls at runtime; small per-call overhead
- Mark overriding functions with
override— the compiler catches mistakes where you thought you were overriding but weren't - Always add a virtual destructor to base classes — without it,
delete base_ptronly calls the base destructor dynamic_castsafely downcasts — returnsnullptr(pointer) or throwsbad_cast(reference) on failure- Abstract classes (with pure virtual functions) define interfaces — they cannot be instantiated
- Design towards virtual interfaces rather than
dynamic_castchains — the latter indicates missing virtual methods
Next, we'll cover operator overloading — giving custom types the ability to use C++ operators naturally.