Polymorphism in C++ — 48-Hour Silent Memory Leak
Deleting derived via base pointer without virtual destructor? Only base destructor runs – your app leaks.
- 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.
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.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.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
That's C++ Basics. Mark it forged?
5 min read · try the examples if you haven't