Skip to content
Home C / C++ Polymorphism in C++ Explained: Static vs Dynamic with Real Examples

Polymorphism in C++ Explained: Static vs Dynamic with Real Examples

Where developers are forged. · Structured learning · Free forever.
📍 Part of: C++ Basics → Topic 6 of 19
Polymorphism in C++ demystified — learn compile-time vs runtime polymorphism, virtual functions, and vtables with real-world code and common gotchas.
⚙️ Intermediate — basic C / C++ knowledge assumed
In this tutorial, you'll learn
Polymorphism in C++ demystified — learn compile-time vs runtime polymorphism, virtual functions, and vtables with real-world code and common gotchas.
  • 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 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
  • 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
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

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.cpp · CPP
123456789101112131415161718192021222324252627282930
#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.

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.cpp · CPP
1234567891011121314151617181920212223242526272829303132333435
#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.

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.cpp · CPP
12345678910111213141516171819202122232425262728293031323334
#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.

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.cpp · CPP
1234567891011121314151617181920212223
#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.
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

  • 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 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
  • 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

    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 —

    Fix

    always declare the destructor virtual in any class that has virtual functions, even if it's just virtual ~Base() = default;

    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 override is completely ignored —

    Fix

    always pass polymorphic objects by pointer or reference (const Shape& or Shape*), never by value

    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 and the bug is nearly invisible —

    Fix

    always use override on every function intended to override a virtual; the compiler then flags any signature mismatch as an error

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-virtual as 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 draw() call. Without it, every new type forces you to edit old code, which breaks existing tests and introduces bugs.

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.

🔥
Naren Founder & Author

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.

← PreviousInheritance in C++Next →Operator Overloading in C++
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged