Senior 5 min · March 06, 2026

Polymorphism in C++ — 48-Hour Silent Memory Leak

Deleting derived via base pointer without virtual destructor? Only base destructor runs – your app leaks.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

CompileTimePolymorphism.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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;
}
Output
Integer: 42
Double: 3.14
Int sum: 30
String sum: Hello Forge
Pro Tip:
Prefer templates over overloading when the logic is identical across types. If you find yourself copy-pasting the same function body just to change int to double, a template is the cleaner solution — and it automatically handles any future type you throw at it.
Production Insight
Template bloat is real – each distinct type instantiates a separate function.
Large template libraries (Boost before modules) can double binary size.
Rule: if binary size is a concern, measure first – don't blindly template everything.
Key Takeaway
Compile-time polymorphism: zero runtime overhead, types must be known at compile time.
Function overloading for varied argument patterns; templates for type-agnostic logic.
The punchline: templates let you write once, stamp out for any type – but watch binary bloat.

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 Circle() and have 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.

RuntimePolymorphism.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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;
}
Output
Circle r=5
[Circle destructor]
[Shape destructor]
How the vtable actually works:
When you mark a function virtual, the compiler builds a per-class table of function pointers (the vtable) and plants a hidden vptr inside every object. A virtual call does two extra memory reads: one to fetch the vptr, one to index into the vtable. That's it — the overhead is tiny, but it does exist. In performance-critical tight loops (game physics, signal processing), this can matter. Everywhere else, it's irrelevant.
Production Insight
Virtual dispatch cost: ~2 extra indirections + possible cache miss.
In hot loops (e.g., per-particle update), replacing virtual calls with templates can yield 20-40% speedup.
But for 99% of code, the flexibility outweighs the tiny overhead – don't over-optimise early.
Key Takeaway
Virtual functions work through a vtable – one per class, one vptr per object.
The cost: two memory reads to resolve the call at runtime.
The punchline: virtual dispatch is cheap; missing virtual destructors are expensive.

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.

PaymentProcessorSystem.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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;
}
Output
Stripe: Processing $99.99
Success via Stripe
Watch Out:
If you forget to implement a pure virtual function in a derived class, the derived class becomes abstract too — and the compiler will refuse to instantiate it with a cryptic 'cannot instantiate abstract type' error. Always check you've implemented every = 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.
Production Insight
When adding a new derived type, you must implement every pure virtual function – the compiler enforces this.
But if the interface grows (e.g., adding a new virtual function to the base), all existing derived classes break.
Rule: lean interfaces (few pure virtuals) are easier to evolve – prefer small, focused abstract classes.
Key Takeaway
Pure virtual functions (= 0) define contracts – derived classes must implement them.
Abstract classes can hold shared state and non-virtual helpers – more flexible than Java interfaces.
The punchline: design abstract classes with minimal surface area; adding new virtuals later breaks every derived 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.

PolymorphismGotchas.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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;
}
Output
Hello from Base
Hello from Derived
The Golden Rule:
If a class has any virtual functions, give it a virtual destructor — even if it's empty. Make it a reflex. The Core C++ Guidelines say it plainly: 'A base class destructor should be either public and virtual, or protected and non-virtual.' There's no middle ground when you're deleting through base-class pointers.
Production Insight
Object slicing is especially nasty in STL containers – vector<Base> copies by value, severing polymorphism.
If you need a polymorphic collection, use vector<unique_ptr<Base>> or vector<Base*>.
Memory leaks from missing virtual destructors are silent in small tests – they only surface under sustained load.
Key Takeaway
Slicing: pass polymorphic objects by pointer or reference, never by value.
Missing virtual destructor: if any virtual function exists, the destructor must be virtual.
The punchline: two silent killers – slicing and non-virtual dtor – both preventable by following simple rules.

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.

OverrideAndFinal.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

class Base {
public:
    virtual void doSomething() const { std::cout << "Base::doSomething const\n"; }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    // void doSomething() override { std::cout << "Derived::doSomething\n"; } 
    // ^ Error: missing const – override catches it
    void doSomething() const override { std::cout << "Derived::doSomething const\n"; } // Correct
    // final prevents further overriding:
    // virtual void finalMethod() final {}
};

class FinalClass final {}; // Cannot be inherited

int main() {
    return 0;
}
Devirtualization:
When a function or class is marked 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.
Production Insight
In codebases with heavy polymorphism, override catches hundreds of signature mismatches per release.
Without 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.
Key Takeaway
override makes the compiler check your intent – use it on every overridden virtual function.
final prevents further overriding and enables devirtualization.
The punchline: override is mandatory for safe code; final is an optimisation tool.
● Production incidentPOST-MORTEMseverity: high

The Silent Memory Leak: Missing Virtual Destructor

Symptom
Memory usage in the payment service grew linearly over 48 hours, eventually triggering out-of-memory kills. No obvious crash – just slow degradation and Kubernetes OOMKilled events.
Assumption
The team assumed that since the base class 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.
Root cause
Deleting a derived object through a base-class pointer when the base destructor is non-virtual invokes undefined behavior – the derived destructor never executes. In practice, only the base part is destroyed, leaking all resources owned by the derived class.
Fix
Change ~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.
Key lesson
  • 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.
Production debug guideSymptom → Action for common polymorphism failures5 entries
Symptom · 01
A function call behaves as if no override exists – base-class implementation runs instead of derived.
Fix
Check if the function is declared 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.
Symptom · 02
Derived object passed to a function produces base-class behaviour, though the pointer seems correct.
Fix
Look for object slicing – is the function parameter typed as Base (by value) instead of Base& or Base*? Change to reference or pointer.
Symptom · 03
Compiler error: 'cannot instantiate abstract class' – but you've implemented every function listed in the base class.
Fix
The derived class may be missing 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.
Symptom · 04
Memory slowly grows after a deletion through a base pointer – no crash, just eventual OOM.
Fix
Check whether the base class destructor is virtual. If not, the derived destructor never runs. This is undefined behavior – fix by making the base destructor virtual.
Symptom · 05
dynamic_cast returns nullptr or bad_cast even though the object appears to be the target type.
Fix
Ensure the class hierarchy has at least one virtual function – RTTI requires a vtable. Also check that you're casting from a pointer (not a value).
★ Polymorphism Quick Debug Cheat SheetWhen polymorphic behaviour goes wrong, these commands and checks pinpoint the issue fast.
Virtual function not called – base version runs.
Immediate action
Check if function is `virtual` in base and exactly overridden in derived.
Commands
g++ -Wall -Wextra -Wnon-virtual-dtor source.cpp
add `override` to derived – compiler errors reveal mismatch.
Fix now
Fix the signature or add virtual keyword to base.
Memory leak after polymorphic deletion.+
Immediate action
Verify base destructor has `virtual` specifier.
Commands
grep 'virtual.*~' header.h (check base class)
valgrind --leak-check=full ./program (confirm leak location)
Fix now
Add virtual ~Base() = default; to base class.
Object slicing – derived info lost.+
Immediate action
Find where derived object is assigned to base by value.
Commands
grep -rn 'Base b = derived' src/ (search for value assignment)
Check function parameters – are they `Base` or `Base&`?
Fix now
Change to Base& or Base*.
Cannot instantiate abstract class.+
Immediate action
Identify which pure virtual function is missing implementation.
Commands
g++ -c derived.cpp (look for 'unimplemented pure virtual method' error)
Add `override` to all overridden functions to catch mismatches.
Fix now
Implement the missing pure virtual function(s) in the derived class.
Feature / AspectCompile-Time (Static) PolymorphismRuntime (Dynamic) Polymorphism
Resolved when?During compilationDuring program execution
MechanismFunction overloading, templatesVirtual functions, vtable
PerformanceZero runtime overhead — inlined by compilerSmall vtable lookup cost per virtual call
FlexibilityTypes must be known at compile timeWorks with types unknown until runtime
Use caseAlgorithms, containers, math operationsPlugin systems, game entities, UI widgets
Error detectionCompile-time type errors caught earlyType errors may surface at runtime
Code bloat riskTemplates can cause binary bloat if overusedMinimal — one vtable per class
Keyword usedtemplate, overloading (no keyword)virtual, override, = 0
Can use abstract base?No — templates work with any conforming typeYes — pure virtual enforces contracts

Key takeaways

1
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
2
Runtime polymorphism needs three things to work correctly
the 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
3
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
4
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
5
Use override on every overriding function to catch signature mismatches at compile time
it's the single best tool to prevent polymorphism bugs

Common mistakes to avoid

4 patterns
×

Forgetting the virtual destructor in a polymorphic base class

Symptom
Derived class destructor never fires when deleting through a base pointer; resources leak silently with no crash or compiler warning. Over time, memory consumption grows until OOM.
Fix
Always declare the destructor 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)

Symptom
Virtual function calls return base-class behaviour even though you passed a derived object. The call uses the base copy, ignoring derived state.
Fix
Always pass polymorphic objects by pointer or reference (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

Symptom
A typo or mismatched const-qualifier silently creates a brand-new function instead of overriding the base. The vtable dispatch calls the base version. No compiler error, just wrong behaviour.
Fix
Always use 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

Symptom
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.
Fix
Ensure the class hierarchy has at least one virtual function (which is already required for polymorphism). If RTTI is disabled globally (-fno-rtti), dynamic_cast always returns nullptr – use static_cast when you are certain of the type, or enable RTTI.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between compile-time and runtime polymorphism in ...
Q02SENIOR
What is a vtable and a vptr? How does C++ use them to implement virtual ...
Q03SENIOR
What happens if you delete a derived class object through a base class p...
Q04SENIOR
Can a constructor be virtual in C++? Explain why or why not based on the...
Q05SENIOR
Explain the 'Diamond Problem' in multiple inheritance and how virtual in...
Q06SENIOR
What is the cost of a virtual function call compared to a regular functi...
Q01 of 06SENIOR

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?

ANSWER
Compile-time polymorphism is resolved by the compiler before execution – achieved through function overloading and templates. Runtime polymorphism uses virtual functions and a vtable, resolved at runtime. Choose compile-time when performance is critical and all types are known in advance (e.g., generic containers like std::vector). Choose runtime when you need to handle types only known at runtime (e.g., a plugin system loading different graphic format decoders).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is polymorphism in C++ and why do we need it?
02
When should I use virtual functions vs function templates in C++?
03
What does 'pure virtual' mean, and what is an abstract class?
04
Does having virtual functions increase the size of an object?
05
Can you have a virtual function that is also static?
🔥

That's C++ Basics. Mark it forged?

5 min read · try the examples if you haven't

Previous
Inheritance in C++
6 / 19 · C++ Basics
Next
Operator Overloading in C++