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
Aspect | Compile-time polymorphism | Run-time polymorphism |
---|---|---|
Resolution time | Decided by the compiler during compilation (early binding/static) | Decided at program execution via dynamic dispatch (late binding/dynamic) |
Typical mechanisms | Function overloading, operator overloading, templates/CRTP | Virtual functions with overriding in class hierarchies |
Binding | Static (early) binding at compile time | Dynamic (late) binding through vtable lookups |
Requires inheritance | No; works without base/derived relationships (e.g., overloading, templates) | Yes; relies on a base class with virtuals and derived overrides |
Dispatch mechanism | Overload resolution or template instantiation chosen by the compiler | Object’s vptr selects the function via its vtable at runtime |
Performance | Usually faster; calls are direct and can be heavily optimized/inlined | Slight overhead from virtual dispatch; typically negligible at higher abstraction levels |
Flexibility | Behavior fixed at build time; less flexible for late choices | Highly flexible; behavior can be selected or swapped at runtime |
Common examples | Overloaded area(...) , templated utilities like add<T> , operator overloading | Virtual Shape::area() overridden by Circle , Rectangle |
Error detection | Mismatches typically caught at compile time during overload/template resolution | Interface mismatches surface through virtual overrides and are exercised at runtime |
ABI/sharing across modules | Template code must be visible to each TU; care needed for code bloat and ODR | Stable virtual interfaces are ABI-friendly and decouple callers from implementations |
Primary use cases | Hot paths, generic algorithms, zero‑overhead abstractions | Plugins, runtime strategy selection, interface-based designs |
Also known as | Static polymorphism, early binding | Dynamic 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 avirtual
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!