Compile-time vs Run-time Polymorphism in C++ with Examples

Here’s a clear, beginner-friendly guide to Compile-time vs Run-time Polymorphism in C++. You’ll learn what each type means, how they work with commented code, and when to use them, with “Try it yourself” challenges after every section.

Think of polymorphism like a universal command: the same call triggers different behaviors. With Compile-time vs Run-time Polymorphism, the difference is about when the decision is made—either during compilation (faster, fixed) or at runtime (flexible, dynamic).

🔹 What is Polymorphism in C++?

Polymorphism means “many forms.” In C++, it lets you use a common interface to call different behaviors. There are two main kinds:

  • Compile-time polymorphism: Resolved by the compiler before the program runs (e.g., function overloading, templates, operator overloading).
  • Run-time polymorphism: Resolved while the program runs using virtual functions and inheritance.

🔹 Compile-time Polymorphism: Function Overloading

#include <iostream>
#include <cmath>
using namespace std;
// Same name, different parameters: resolved
// at compile time
int area(int side) { // square
    return side * side;
}
int area(int w, int h) { // rectangle
    return w * h;
}
double area(double r) { // circle (overload by type)
    return M_PI * r * r;
}
int main() {
    cout << "Square: " << area(4) << "\n"; 
    // calls area(int)
    cout << "Rectangle: " << area(4, 5) << "\n"; 
    // calls area(int,int)
    cout << "Circle: " << area(2.5) << "\n"; 
    // calls area(double)
}

Output

Square: 16
Rectangle: 20
Circle: 19.6349

🔹 Compile-time Polymorphism: Templates

#include <iostream>
using namespace std;
// Generic add: works for int, double, string
// (if + is defined)
template <typename T>
T add(T a, T b) {
    return a + b; // requires operator+ for T
}
int main() {
    cout << add(2, 3) << "\n"; // int
    cout << add(2.5, 1.2) << "\n"; // double
    cout << add(string("Hi "),
               string("there")) << "\n"; // string
}

Output

5
3.7
Hi there

🔹 Compile-time Polymorphism: Operator Overloading

#include <iostream>
using namespace std;
struct Vec2 {
    double x{0}, y{0};
    // Add two vectors (compile-time resolution)
    Vec2 operator+(const Vec2& other) const {
        return {x + other.x, y + other.y};
    }
};
// Stream output for Vec2
ostream& operator<<(ostream& os, const Vec2& v) {
    return os << "(" << v.x
              << ", " << v.y << ")";
}
int main() {
    Vec2 a{1, 2}, b{3, 4};
    cout << (a + b) << "\n"; 
    // chooses operator+ for Vec2 at compile time
}

Output

(4, 6)

🔹 Run-time Polymorphism: Virtual Functions

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Shape {
public:
    virtual ~Shape() = default;
    // virtual destructor for safe
    // polymorphic deletion
    virtual double area() const = 0;
    // pure virtual: must be overridden
    virtual const char* name() const {
        return "Shape";
    }
};
class Circle : public Shape {
    double r;
public:
    explicit Circle(double radius) : r(radius) {}
    double area() const override {
        return 3.1415926535 * r * r;
    }
    const char* name() const override {
        return "Circle";
    }
};
class Rectangle : public Shape {
    double w, h;
public:
    Rectangle(double w_, double h_)
        : w(w_), h(h_) {}
    double area() const override {
        return w * h;
    }
    const char* name() const override {
        return "Rectangle";
    }
};
int main() {
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>(3.0));
    shapes.push_back(
        make_unique<Rectangle>(4.0, 5.0));
    for (const auto& s : shapes) {
        cout << s->name()
             << " area: "
             << s->area() << "\n";
        // runtime dispatch
    }
}

Output

Circle area: 28.2743
Rectangle area: 20

🔹 Compile-time Polymorphism Vs Run-time Polymorphism

AspectCompile-time polymorphismRun-time polymorphism
Resolution timeDecided by the compiler during compilation (early binding/static)Decided at program execution via dynamic dispatch (late binding/dynamic)
Typical mechanismsFunction overloading, operator overloading, templates/CRTPVirtual functions with overriding in class hierarchies
BindingStatic (early) binding at compile timeDynamic (late) binding through vtable lookups
Requires inheritanceNo; works without base/derived relationships (e.g., overloading, templates)Yes; relies on a base class with virtuals and derived overrides
Dispatch mechanismOverload resolution or template instantiation chosen by the compilerObject’s vptr selects the function via its vtable at runtime
PerformanceUsually faster; calls are direct and can be heavily optimized/inlinedSlight overhead from virtual dispatch; typically negligible at higher abstraction levels
FlexibilityBehavior fixed at build time; less flexible for late choicesHighly flexible; behavior can be selected or swapped at runtime
Common examplesOverloaded area(...), templated utilities like add<T>, operator overloadingVirtual Shape::area() overridden by Circle, Rectangle
Error detectionMismatches typically caught at compile time during overload/template resolutionInterface mismatches surface through virtual overrides and are exercised at runtime
ABI/sharing across modulesTemplate code must be visible to each TU; care needed for code bloat and ODRStable virtual interfaces are ABI-friendly and decouple callers from implementations
Primary use casesHot paths, generic algorithms, zero‑overhead abstractionsPlugins, runtime strategy selection, interface-based designs
Also known asStatic polymorphism, early bindingDynamic polymorphism, late binding

🔹 Mini Project: Sorters (Both Styles)

We’ll implement sorting in two ways to showcase Compile-time vs Run-time Polymorphism. One uses templates (compile-time), the other uses virtual interfaces (run-time).

Compile-time version (template comparator):

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
// Generic sort using a template comparator
// (compile-time polymorphism)
template <typename T, typename Compare>
void sortWith(vector<T>& v, Compare comp) {
    sort(v.begin(), v.end(), comp);
    // comp is inlined by the compiler
}
int main() {
    vector<int> nums{5, 2, 9, 1, 5, 6};
    sortWith(nums, [](int a, int b){
        return a < b;
    }); // ascending
    for (int x : nums)
        cout << x << " ";
    cout << "\n";
}

Output

1 2 5 5 6 9

Run-time version (virtual interface):

#include <algorithm>
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
struct Comparator {
    virtual ~Comparator() = default;
    virtual bool less(int a, int b) const = 0;
    // run-time dispatch
};
struct Asc : Comparator {
    bool less(int a, int b) const override {
        return a < b;
    }
};
struct Desc : Comparator {
    bool less(int a, int b) const override {
        return a > b;
    }
};
void sortWith(vector<int>& v,
              const Comparator& comp) {
    sort(v.begin(), v.end(),
         [&comp](int a, int b){
             return comp.less(a, b);
         });
}
int main() {
    vector<int> nums{5, 2, 9, 1, 5, 6};
    Asc asc;
    Desc desc;
    sortWith(nums, asc);
    for (int x : nums)
        cout << x << " ";
    cout << "\n";
    sortWith(nums, desc);
    for (int x : nums)
        cout << x << " ";
    cout << "\n";
}

Output

1 2 5 5 6 9
9 6 5 5 2 1

Templates give speed and inlining, while the virtual interface gives late binding and pluggability (you can load a comparator at runtime).

Try it yourself

  • Add a EvenFirst comparator to group even numbers before odds (maintain order within groups).
  • Make a template comparator for strings that ignores case.

🔹 Performance Tips and Best Practices

  • Prefer compile-time polymorphism (templates) for hot code paths to avoid virtual-call overhead.
  • Use run-time polymorphism when implementations change at runtime (plugins, strategy selection).
  • Add override to all overrides and a virtual destructor to base classes used polymorphically.
  • Be mindful of code bloat from excessive template instantiations; use type-erasure or virtuals when needed.
  • Document why you chose compile-time vs run-time for maintainers.

Try it yourself

  • Micro-benchmark a virtual call vs an inlined function using chrono to see typical overhead.
  • Refactor a virtual-based module into templates and compare binary size and speed.

🔹 FAQs about Compile-time vs Run-time Polymorphism

Q1: Which is faster—compile-time or run-time polymorphism?
Compile-time polymorphism is usually faster since the call is resolved and optimized (often inlined) by the compiler. Run-time adds a tiny vtable lookup overhead.

Try it: Write a loop calling a template function vs a virtual function a million times and measure with chrono.

Q2: Can templates replace virtual functions?
Sometimes. Templates work when behavior is known at compile time. If you need to choose implementations at runtime (e.g., load from config), virtuals fit better.

Try it: Build both a template-based and a virtual-based logger and switch them via a command-line flag.

Q3: Do templates increase binary size?
They can, due to multiple instantiations for different types. Link-time optimization and reducing type variety helps. Virtuals keep one implementation per function.

Try it: Instantiate a function template with many types and compare binary size with a virtual solution.

Q4: Is function overloading the same as overriding?
No. Overloading is compile-time (same name, different parameters). Overriding is run-time (same signature in derived class with virtual).

Try it: Create both in a small example and observe when each is chosen.

Q5: Can I mix both polymorphism types?
Yes. Many systems use templates for inner loops and virtuals for high-level plug-in points (e.g., STL uses templates; GUI frameworks use virtuals).

Try it: Use templates for a math kernel and a virtual interface to select a kernel at runtime.

Q6: Do I need a virtual destructor in base classes?
Yes, if you plan to delete derived objects through base pointers; otherwise destructors may not run correctly.

Try it: Remove virtual from a base destructor and delete a derived object via a base pointer—observe destructor order.

Q7: Are default arguments affected by virtual dispatch?
No. Default arguments are bound to the static type at compile time, even if the function body is dispatched virtually.

Try it: Call a virtual with a default argument via a base pointer and see that the base default is used.

Q8: What about CRTP (Curiously Recurring Template Pattern)?
CRTP is a template technique that enables static polymorphism (no virtuals) by making a base class take the derived class as a template parameter.

Try it: Implement a CRTP base with a draw() method that calls static_cast<Derived*>(this)->drawImpl().

Q9: Can operator overloading be run-time polymorphism?
Operator overloading itself is compile-time, but operator bodies can use virtual calls internally if needed.

Try it: Overload operator<< to call a virtual print() method on a base pointer.

Q10: Which should I learn first?
Start with virtual functions and simple templates. Then explore advanced templates and patterns once you’re comfortable.

Try it: Recreate the examples in this article in your IDE and extend them with your own types.

🔹 Wrapping Up

Compile-time vs Run-time Polymorphism is about choosing when decisions are made: at build time for speed and type-generic code, or at runtime for flexibility and extensibility. Use templates and overloading for compile-time power, and virtual functions for dynamic behavior. Practice the “Try it yourself” tasks to master both styles quickly. Happy coding!

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.