Polymorphism in C++ Explained: Static vs Dynamic with Real Examples
- Compile-time polymorphism (overloading + templates) is resolved by the compiler with zero runtime cost — use it when all types are known upfront and performance matters
- Runtime polymorphism needs three things to work correctly: the
virtualkeyword in the base class, a matchingoverridein the derived class, and avirtualdestructor in the base — miss any one and you get subtle bugs - Object slicing is a silent killer — polymorphic behaviour only works through pointers and references, never through value copies; make this a reflex whenever you're working with inheritance hierarchies
Imagine a universal TV remote. One button says 'Play' — but press it on a DVD player and it spins a disc, press it on a streaming box and it starts a video, press it on a music player and it plays a song. Same button, totally different behavior depending on what device you're holding. That's polymorphism. One interface, many forms of behavior underneath. You don't need a different remote for every device — the remote just figures it out.
Every serious C++ codebase leans on polymorphism. It's the reason you can write a game engine that handles dozens of enemy types through a single pointer, or a graphics renderer that draws circles, rectangles, and polygons with one unified call. Without it, you'd be writing a separate function for every type you ever invent — and every time you add a new type, you'd be back editing old code. That's a maintenance nightmare waiting to happen.
Polymorphism solves the rigidity problem. It lets you write code that works with types that don't even exist yet. You define a contract — an interface — and any class that honours that contract gets to play. The caller doesn't need to know the specifics. This is what separates beginner C++ from production-grade C++. It's the difference between code that's bolted together and code that's designed to grow.
By the end of this article you'll understand the two flavours of polymorphism — compile-time and runtime — and exactly when to reach for each one. You'll see how virtual functions and vtables actually work under the hood, avoid the destructor trap that silently corrupts memory, and walk away with the mental model that makes C++ object-oriented design finally make sense.
Compile-Time Polymorphism: Function Overloading and Templates
Compile-time polymorphism — also called static polymorphism — is resolved before your program ever runs. The compiler looks at the call site, figures out which version of a function to invoke, and hard-wires that decision into the binary. There's zero runtime cost. Two mechanisms drive it: function overloading and templates.
Function overloading lets you define multiple functions with the same name but different parameter signatures. The compiler picks the right one based on the arguments you pass. Think of a print function that handles integers, floats, and strings — same name, compiler figures out which one fits.
Templates go further. They let you write one function or class that works for any type, and the compiler stamps out a concrete version per type at compile time. This is how std::vector<int> and std::vector<std::string> both exist without you writing two separate vector implementations.
Use compile-time polymorphism when you know all your types upfront and want maximum performance. Templates are especially powerful for data structures and algorithms where the logic is identical regardless of type.
#include <iostream> #include <string> // io.thecodeforge package branding convention used in architectural comments // --- Function Overloading --- void displayValue(int number) { std::cout << "Integer: " << number << "\n"; } void displayValue(double decimal) { std::cout << "Double: " << decimal << "\n"; } // --- Function Template --- template <typename T> T addTwo(T first, T second) { return first + second; } int main() { displayValue(42); displayValue(3.14); std::cout << "Int sum: " << addTwo(10, 20) << "\n"; std::cout << "String sum: " << addTwo(std::string("Hello "), std::string("Forge")) << "\n"; return 0; }
Double: 3.14
Int sum: 30
String sum: Hello Forge
int to double, a template is the cleaner solution — and it automatically handles any future type you throw at it.Runtime Polymorphism: Virtual Functions and the vtable
Runtime polymorphism is where C++ gets genuinely powerful — and genuinely dangerous if you don't understand the machinery. The core mechanism is the virtual keyword, and it works through something called a vtable (virtual dispatch table).
When you mark a function virtual in a base class, the compiler attaches a hidden pointer — the vptr — to every object of that class. That pointer points to a vtable: a lookup table of function pointers specific to the object's actual type. When you call a virtual function through a base-class pointer, the runtime consults that vtable and calls the right version.
This is what lets you write Shape s = new and have Circle()s->draw() call Circle::draw() rather than Shape::draw(). The pointer type is Shape, but the object behind it knows it's a Circle.
The critical rule: if a class has any virtual functions, its destructor must also be virtual. Skip this and you'll get partial destruction — the derived class's destructor never fires, leaking resources. We'll revisit this in the gotchas section.
#include <iostream> #include <vector> #include <memory> #include <cmath> namespace io_thecodeforge { class Shape { public: virtual double area() const = 0; virtual void describe() const = 0; virtual ~Shape() { std::cout << "[Shape destructor]\n"; } }; class Circle : public Shape { public: explicit Circle(double r) : r_(r) {} double area() const override { return M_PI * r_ * r_; } void describe() const override { std::cout << "Circle r=" << r_ << "\n"; } ~Circle() override { std::cout << "[Circle destructor]\n"; } private: double r_; }; } int main() { using namespace io_thecodeforge; std::vector<std::unique_ptr<Shape>> shapes; shapes.push_back(std::make_unique<Circle>(5.0)); for (const auto& s : shapes) { s->describe(); } return 0; }
[Circle destructor]
[Shape destructor]
Abstract Classes vs Interfaces: Designing a Real Extensible System
An abstract class in C++ is any class with at least one pure virtual function (= 0). You can't instantiate it directly — it exists purely as a contract for derived classes to fulfill. This is C++'s version of an interface, though with more flexibility since abstract classes can also carry shared state and non-virtual implementation.
The real power shows up when you're designing a system that needs to be extended without modifying existing code — this is the Open/Closed Principle in action. You define the abstract base once. New types slot in by subclassing and implementing the contract. Your existing calling code never changes.
Here's a practical example: a payment processing system. You have a PaymentProcessor abstract class. Stripe, PayPal, and a future Bitcoin processor all implement it. Your checkout logic calls processor->charge(amount) — it doesn't know or care which processor is behind the pointer.
#include <iostream> #include <string> #include <memory> namespace io_thecodeforge { class PaymentProcessor { public: virtual bool charge(double amount) = 0; virtual std::string getName() const = 0; virtual ~PaymentProcessor() = default; }; class StripeProcessor : public PaymentProcessor { public: bool charge(double amount) override { std::cout << "Stripe: Processing $" << amount << "\n"; return true; } std::string getName() const override { return "Stripe"; } }; } void runCheckout(io_thecodeforge::PaymentProcessor& p, double amount) { if (p.charge(amount)) { std::cout << "Success via " << p.getName() << "\n"; } } int main() { io_thecodeforge::StripeProcessor stripe; runCheckout(stripe, 99.99); return 0; }
Success via Stripe
= 0 function. The override keyword is your safety net: it causes a compile error if the signature doesn't match any virtual function in the base class.Gotchas: The Two Mistakes That Actually Bite People
Polymorphism in C++ is powerful but it has sharp edges. Two mistakes in particular show up constantly in code reviews and debugging sessions. Both are silent — no crash on the obvious line, just wrong behaviour or a memory leak that takes hours to track down.
The first is slicing. It happens when you assign a derived object to a base object by value. The derived parts get 'sliced off' — copied away — leaving only the base portion. The virtual dispatch mechanism works through pointers and references, not values. The moment you copy by value, you lose polymorphic behaviour entirely.
The second is the missing virtual destructor. If your base class destructor isn't virtual and you delete a derived object through a base pointer, only the base destructor fires. Any resources owned by the derived class — heap memory, file handles, network sockets — are never released. Valgrind will scream at you. Your users will file memory leak bugs.
#include <iostream> class Base { public: virtual void greet() { std::cout << "Hello from Base\n"; } // virtual ~Base() = default; // If missing, deleting via Base* is UB }; class Derived : public Base { public: void greet() override { std::cout << "Hello from Derived\n"; } }; int main() { Derived d; Base sliced = d; // OBJECT SLICING sliced.greet(); // Prints Base message Base& ref = d; // CORRECT ref.greet(); // Prints Derived message return 0; }
Hello from Derived
| Feature / Aspect | Compile-Time (Static) Polymorphism | Runtime (Dynamic) Polymorphism |
|---|---|---|
| Resolved when? | During compilation | During program execution |
| Mechanism | Function overloading, templates | Virtual functions, vtable |
| Performance | Zero runtime overhead — inlined by compiler | Small vtable lookup cost per virtual call |
| Flexibility | Types must be known at compile time | Works with types unknown until runtime |
| Use case | Algorithms, containers, math operations | Plugin systems, game entities, UI widgets |
| Error detection | Compile-time type errors caught early | Type errors may surface at runtime |
| Code bloat risk | Templates can cause binary bloat if overused | Minimal — one vtable per class |
| Keyword used | template, overloading (no keyword) | virtual, override, = 0 |
| Can use abstract base? | No — templates work with any conforming type | Yes — pure virtual enforces contracts |
🎯 Key Takeaways
- Compile-time polymorphism (overloading + templates) is resolved by the compiler with zero runtime cost — use it when all types are known upfront and performance matters
- Runtime polymorphism needs three things to work correctly: the
virtualkeyword in the base class, a matchingoverridein the derived class, and avirtualdestructor in the base — miss any one and you get subtle bugs - Object slicing is a silent killer — polymorphic behaviour only works through pointers and references, never through value copies; make this a reflex whenever you're working with inheritance hierarchies
- Abstract classes with pure virtual functions (
= 0) are C++'s way of defining interfaces — they enforce contracts on derived classes and let you write calling code that works for types that don't exist yet
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between compile-time and runtime polymorphism in C++? Can you give a real-world example of when you'd choose one over the other?
- QWhat is a vtable and a vptr? How does C++ use them to implement virtual dispatch, and what is the memory cost of having a virtual function in a class?
- QWhat happens if you delete a derived class object through a base class pointer when the base class destructor is not virtual? How does making it virtual fix the problem, and why does the standard recommend
protected non-virtualas an alternative? - QCan a constructor be virtual in C++? Explain why or why not based on the object's initialization sequence.
- QExplain the 'Diamond Problem' in multiple inheritance and how virtual inheritance solves it. How does this impact the vtable layout?
- QWhat is the cost of a virtual function call compared to a regular function call? Mention cache misses and branch prediction impact.
Frequently Asked Questions
What is polymorphism in C++ and why do we need it?
Polymorphism lets you write code that works with many different types through a single interface. You need it because it lets you add new types to a system without modifying existing code — a Circle, Rectangle, and any future shape you invent can all be handled by the same call. Without it, every new type forces you to edit old code, which breaks existing tests and introduces bugs.draw()
When should I use virtual functions vs function templates in C++?
Use virtual functions (runtime polymorphism) when you're dealing with objects whose exact types aren't known until runtime — think loading plugins, processing user input, or iterating a mixed container of derived types. Use templates (compile-time polymorphism) when you know all your types at compile time and want zero runtime overhead — data structures, mathematical algorithms, and generic utilities are classic template territory.
What does 'pure virtual' mean, and what is an abstract class?
A pure virtual function is declared with = 0 — it has no implementation in the base class and forces every derived class to provide one. Any class that contains at least one pure virtual function becomes an abstract class, which means you cannot create instances of it directly. Abstract classes serve as contracts or interfaces, ensuring all derived classes implement a consistent set of behaviours.
Does having virtual functions increase the size of an object?
Yes. Adding the first virtual function to a class typically adds a single pointer (the vptr) to the object's memory layout. On a 64-bit system, this usually adds 8 bytes to every instance of that class. Subsequent virtual functions do not increase the object size further, as they are simply additional entries in the class-wide vtable.
Can you have a virtual function that is also static?
No. A static member function does not have a this pointer, which is required for the virtual dispatch mechanism to identify which specific object's vtable to look up. Virtual functions belong to the instance's runtime type, while static functions belong to the class scope.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.