Polymorphism in C++ — 48-Hour Silent Memory Leak
Deleting derived via base pointer without virtual destructor? Only base destructor runs – your app leaks.
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
- Polymorphism lets one interface serve many implementations – the caller doesn’t need to know the concrete type
- Compile-time (static) uses overloading and templates – zero runtime cost, resolved before the program runs
- Runtime (dynamic) uses virtual functions through a vtable – one pointer per object, one lookup per call
- Performance cost of virtual dispatch: ~2 extra memory reads, negligible outside hot loops
- Production killer: missing virtual destructor causes silent resource leaks – Valgrind won’t catch it during testing
- Biggest mistake: object slicing – passing by value instead of pointer/reference, polymorphic behaviour silently lost
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.
Polymorphism in C++ — The Virtual Dispatch Tax
Polymorphism in C++ lets a single interface control multiple types through virtual functions and dynamic dispatch. The core mechanic: a base class declares a virtual method, derived classes override it, and calls through a base pointer or reference resolve at runtime to the correct override via the vtable. This is not free — each virtual call incurs an indirect jump and a vtable lookup, roughly 2–3 extra instructions compared to a direct call.
At runtime, each polymorphic object carries a hidden vptr pointing to its class's vtable. The vtable is a static array of function pointers, one per virtual function. When you call ptr->foo(), the compiler emits code that loads the vptr, indexes into the vtable, and jumps to the address stored there. This indirection prevents inlining and devirtualization, which can cost 5–15% performance in tight loops. The vtable itself is per-class, not per-object, so memory overhead is just one pointer per object (8 bytes on 64-bit).
Use polymorphism when you need runtime type flexibility — event systems, plugin architectures, or state machines where the concrete type is not known until runtime. Avoid it in hot paths or when the type is known at compile time; prefer templates (static polymorphism) or std::variant with visitation. In real systems, overusing virtual dispatch is a common source of cache misses and branch mispredictions, especially in game engines or high-frequency trading.
__dynamic_cast and indirect branch resolution, not in business logic.perf stat -e branch-misses — if >5% of branches miss, refactor hot paths to templates or std::variant.std::variant when the type set is fixed at compile time.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.
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.
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.
= 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.
vector<Base> copies by value, severing polymorphism.vector<unique_ptr<Base>> or vector<Base*>.The `override` and `final` Specifiers: Enforcing Intent
Modern C++ (C++11 and later) gives us two powerful specifiers that catch mistakes at compile time: override and final. They don't change runtime behavior, but they document intent and turn subtle bugs into loud compiler errors.
override tells the compiler: 'I intend this function to override a base-class virtual function.' If the base signature doesn't match (typo, missing const, different parameter type), the compiler emits an error instead of silently creating a new function that shadows the base version.
final does the opposite: it prevents further overriding. Mark a virtual function or entire class as final to lock down the design. This can improve performance by enabling devirtualization – the compiler may inline the call because it knows no derived class can override it.
Use override on every function that overrides a virtual – it's as essential as a seatbelt. Use final when you want to guarantee that a specific behaviour is the last word in the hierarchy.
final, the compiler can sometimes skip the vtable lookup entirely – this is called devirtualization. It's a free performance win, especially in tight loops where the same virtual function is called on the same concrete type repeatedly.override catches hundreds of signature mismatches per release.override, a typo like Print() instead of print() creates a new function – the base version runs silently.final on a class allows the compiler to fully devirtualize all calls, yielding up to 2x speed in some hot paths.override makes the compiler check your intent – use it on every overridden virtual function.final prevents further overriding and enables devirtualization.override is mandatory for safe code; final is an optimisation tool.Why Polymorphism? The Real Reason You Need It (It's Not About Animals)
Every junior asks 'why not just if-else chains?' Here's the answer: maintainability and growth.
Without polymorphism, every new shape means adding an else-if to every switch statement in the codebase. Three engineers touch the same file in a sprint. Merge conflicts. Bugs. You rewrite the same logic six times because each team forgot to update 'their' switch.
Polymorphism lets you add new behavior without touching existing code. That's the Open/Closed Principle in action: open for extension, closed for modification.
The real win? When a new hire adds a new subclass and everything just works. No hunting through conditionals. No 'but I didn't know that file existed.'
Type erasure happens at compile-time for templates; virtual dispatch happens at runtime. Both give you the same thing: a single interface that adapts to the concrete type. One is zero-cost abstraction. The other buys you runtime flexibility at the cost of a vtable lookup.
Choose the tool for the job, not because 'polymorphism sounds cool.'
draw() call, miss one, and ship a silent failure. Polymorphic dispatch eliminates that entire class of bug.Pure Virtual Functions: Enforcing a Contract (Not Optional Overrides)
A virtual function with a default implementation is a hint. A pure virtual function (= 0) is a contract.
When you declare a pure virtual function, you tell the compiler: 'This class is incomplete. Anyone who inherits from me must implement this, or they cannot be instantiated.'
This is how you design frameworks. Your base class says what must happen; derived classes decide how.
For example, a Plugin base class might require init(), execute(), and cleanup(). Every plugin must implement all three. No 'I forgot'. No 'it worked on my machine'. The compiler enforces the contract.
This is the cornerstone of interface-based design. In C++, abstract classes with pure virtual functions are your interface keyword. Use them aggressively for any system where multiple implementations are expected.
Don't make methods virtual 'just in case'. Make them pure virtual when you genuinely cannot define a meaningful default. If you can provide a sensible default, do it. If you can't, force the child to define it.
Your future self — and every developer who inherits your code — will thank you.
Runtime vs Compile-Time: When Each Will Save Your Career
Compile-time polymorphism (templates, overloading) resolves at compile time. Zero runtime cost. The compiler generates specialized code for each type you use. This is your go-to for performance-critical code: containers, algorithms, numeric libraries.
But templates cannot be stored in heterogeneous containers. You cannot have a vector<Animal*> of template instantiations. They don't share a common base type unless you manually erase the type — and type erasure is its own complexity.
Runtime polymorphism (virtual functions) resolves at runtime via the vtable. One extra pointer dereference per call — often cached, measurable but rarely catastrophic. The payoff? You can store any derived class in a vector<Base*> and dispatch correctly.
Here's the decision: if the types are known at compile time and won't change — use templates. If you're building a plugin system, event dispatcher, or any architecture where new types arrive as dynamic libraries — use virtual functions.
Mixing both is normal. std::function uses type erasure (runtime) over templated callables (compile-time). Know the cost of each and pick consciously.
The Silent Memory Leak: Missing Virtual Destructor
PaymentGateway didn't own any resources directly, its destructor didn't need to be virtual. The derived StripeGateway class had a pool of database connections to close, but the team thought deleting through the base pointer would call the derived destructor automatically.~PaymentGateway() to virtual ~PaymentGateway() = default; in the base class. The derived destructor then fires correctly. Adding a virtual specifier to every base class that has any virtual function prevents this class of bug entirely.- If a class has any virtual function, its destructor must be virtual – even if it does nothing.
- Make it a reflex: every polymorphic base must have
virtual ~.Base()= default; - Use
unique_ptr<Base>with custom deleter only as a workaround – proper virtual destructor is the right fix.
virtual in the base class. Then verify the derived signature matches exactly – missing const or a different parameter type silently creates a new function instead of an override.Base (by value) instead of Base& or Base*? Change to reference or pointer.override on one function, hiding a mismatch. Add override to every implemented virtual function. The compiler will pinpoint the exact signature that doesn't match.virtual. If not, the derived destructor never runs. This is undefined behavior – fix by making the base destructor virtual.dynamic_cast returns nullptr or bad_cast even though the object appears to be the target type.g++ -Wall -Wextra -Wnon-virtual-dtor source.cppadd `override` to derived – compiler errors reveal mismatch.Key takeaways
virtual keyword in the base class, a matching override in the derived class, and a virtual destructor in the base — miss any one and you get subtle bugs= 0) are C++'s way of defining interfacesoverride on every overriding function to catch signature mismatches at compile timeCommon mistakes to avoid
4 patternsForgetting the virtual destructor in a polymorphic base class
virtual in any class that has virtual functions, even if it's just virtual ~Base() = default;. Use -Wnon-virtual-dtor compiler flag to catch violations.Passing derived objects by value to functions expecting a base type (object slicing)
const Shape& or Shape*). Never use Base as a function parameter type when you need polymorphism. Consider std::unique_ptr<Base> for owning pointers.Omitting the `override` keyword on derived class method signatures
override on every function intended to override a virtual. The compiler then flags any signature mismatch as an error. Make it a team policy – add to coding standards and enable -Wsuggest-override.Assuming `dynamic_cast` works without RTTI enabled
dynamic_cast from base to derived pointer returns nullptr at runtime, even though the object is of the correct derived type. No compile-time error, but the cast fails silently.-fno-rtti), dynamic_cast always returns nullptr – use static_cast when you are certain of the type, or enable RTTI.Interview Questions on This Topic
What 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?
std::vector). Choose runtime when you need to handle types only known at runtime (e.g., a plugin system loading different graphic format decoders).Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
That's C++ Basics. Mark it forged?
8 min read · try the examples if you haven't