C++ Polymorphism - Complete Guide
Understanding Polymorphism
Polymorphism is a core object-oriented programming concept that allows different object types to be accessed through a common interface. In C++, polymorphism provides:
- Runtime flexibility: The actual function call is determined at execution time
- Interface consistency: Different objects can be handled through a shared base interface
- Extensibility: New types can be added without changing existing client code
C++ achieves polymorphism primarily through inheritance and virtual functions.
#include <iostream>
class Shape {
public:
virtual void draw() const { // Base class virtual function
std::cout << "Drawing generic shape\n";
}
virtual ~Shape() = default; // Virtual destructor
};
class Circle : public Shape {
public:
void draw() const override { // Overridden function
std::cout << "Drawing circle\n";
}
};
void drawShape(const Shape& s) {
s.draw(); // Polymorphic call
}
int main() {
Circle c;
drawShape(c); // Outputs "Drawing circle"
}
Drawing circle
Virtual Functions Deep Dive
Virtual functions enable runtime polymorphism using these mechanisms:
- Virtual Table (vtable): Each polymorphic class maintains a table of function pointers
- Dynamic Dispatch: The function to execute is resolved at runtime according to the actual object type
Key points to remember:
- Declare base functions as virtual
to allow overriding
- Use the override
specifier in derived classes for clarity and safety
- Ensure signatures match exactly between base and derived classes
#include <string>
class Animal {
public:
virtual ~Animal() = default; // Base classes for polymorphism should have virtual dtors
virtual std::string sound() const {
return "Some animal sound";
}
// Pure virtual function makes the class abstract
virtual std::string species() const = 0;
};
class Dog : public Animal {
public:
std::string sound() const override {
return "Woof!";
}
std::string species() const override {
return "Canis lupus familiaris";
}
};
Types of Polymorphism
Type | Mechanism | When Used |
---|---|---|
Runtime (Dynamic) | Virtual functions, inheritance | When behavior must vary at runtime |
Compile-time (Static) | Function overloading, templates | When behavior is resolved at compile time |
Parametric | Templates | When the same algorithm should work with different types |
Ad-hoc | Operator/function overloading | When defining specialized behavior for specific types |
Advanced Polymorphism Techniques
Modern C++ provides advanced ways to refine polymorphic behavior:
- final specifier: Prevents further overriding of a class or method
- Covariant return types: Allows overrides to return more specific pointer/reference types
- dynamic_cast: Enables safe downcasting at runtime
- Type erasure: Implemented via wrappers like std::function
or custom erasure types (pimpl/any/variant-based designs)
// Covariant return types apply to pointer/reference returns
class Base {
public:
virtual ~Base() = default;
virtual Base* clone() const = 0; // ownership: raw pointer is returned; caller must manage it
};
class Derived : public Base {
public:
Derived* clone() const override { // Covariant return: Derived* instead of Base*
return new Derived(*this);
}
void specificMethod() const { /* ... */ }
};
#include <memory>
int main() {
std::unique_ptr<Base> b = std::make_unique<Derived>();
// Safe downcast via dynamic_cast
if (auto* d = dynamic_cast<Derived*>(b.get())) {
d->specificMethod();
}
// If you use clone() with raw pointers, wrap immediately to avoid leaks
std::unique_ptr<Base> copy{ b->clone() }; // takes ownership
}
Performance Considerations
Polymorphism introduces small overhead, mainly from:
- vtable lookup: An additional pointer indirection per virtual call
- Object size: Polymorphic objects typically store a vptr (one pointer’s worth of space)
- Cache effects: Indirect jumps can cause branch prediction misses
Practical optimizations:
- Avoid virtual calls in tight, performance-critical loops on hot paths
- Mark classes/methods final
when further overriding is unnecessary (enables devirtualization)
- Apply CRTP or templates for compile-time polymorphism when runtime flexibility is not needed
Real-World Example: Graphics System
#include <iostream>
#include <vector>
#include <memory>
class Graphic {
public:
virtual ~Graphic() = default;
virtual void draw() const = 0;
virtual void scale(double factor) = 0;
virtual std::unique_ptr<Graphic> clone() const = 0; // modern, ownership-safe clone
};
class Circle : public Graphic {
double radius{};
public:
explicit Circle(double r) : radius(r) {}
void draw() const override {
std::cout << "Drawing circle with radius " << radius << '\n';
}
void scale(double factor) override {
radius *= factor;
}
std::unique_ptr<Graphic> clone() const override {
return std::make_unique<Circle>(*this);
}
};
class CompositeGraphic : public Graphic {
std::vector<std::unique_ptr<Graphic>> children;
public:
void add(std::unique_ptr<Graphic> g) {
children.push_back(std::move(g));
}
void draw() const override {
for (const auto& g : children) {
g->draw();
}
}
void scale(double factor) override {
for (auto& g : children) {
g->scale(factor);
}
}
std::unique_ptr<Graphic> clone() const override {
auto comp = std::make_unique<CompositeGraphic>();
for (const auto& g : children) {
comp->add(g->clone());
}
return comp;
}
};
int main() {
CompositeGraphic design;
design.add(std::make_unique<Circle>(5.0));
design.add(std::make_unique<Circle>(10.0));
auto copy = design.clone();
copy->scale(2.0);
std::cout << "Original:" << '\n';
design.draw();
std::cout << "\nScaled copy:" << '\n';
copy->draw();
}
Original: Drawing circle with radius 5 Drawing circle with radius 10 Scaled copy: Drawing circle with radius 10 Drawing circle with radius 20