C++ OOP
Polymorphism
Complete lesson for Polymorphism using LEARNCODEGURU’s 6-level learning model — from basics to advanced practice, with C++ OOP and STL where relevant.
Basics — Polymorphism
Section: C++ OOP. C++ polymorphism — one interface, many forms: compile-time overloading and runtime virtual dispatch.
What is polymorphism?
- **Polymorphism** means “**many forms**” — the same interface can behave differently.
- In C++, you mostly meet two flavours: **compile-time** and **runtime**.
- It lets you write flexible code: call one name (
print,+,draw) and get the right behaviour. - **Real-life:** a “Pay” button — UPI, card, or wallet each pay differently behind the same action.
- **Real-life:**
+adds numbers or concatenates strings depending on operands.
| Idea | Meaning | C++ tools |
|---|---|---|
| One name | Same symbol / call style | add, operator+, draw |
| Many forms | Different implementations | Overloads / overrides |
| Compile-time | Resolved before the program runs | Overloading, templates |
| Runtime | Resolved while the program runs | Virtual functions |
Types of polymorphism in C++
- **Compile-time (static) polymorphism** — binding decided by the compiler.
- **Runtime (dynamic) polymorphism** — binding decided using the actual object type at run time.
| Type | Also called | Main techniques | Binding |
|---|---|---|---|
| Compile-time | Static polymorphism | Function overloading, operator overloading, templates | Early / static |
| Runtime | Dynamic polymorphism | Virtual functions + inheritance | Late / dynamic |
Compile-time vs runtime (compare)
- Both are polymorphism; they differ in **when** the correct version is chosen.
- Compile-time is usually faster (no vtable lookup); runtime is more flexible for plug-in hierarchies.
| Aspect | Compile-time | Runtime |
|---|---|---|
| When decided | Compile time | Run time |
| Typical tools | Overloading, templates | virtual + base pointer/ref |
| Needs inheritance? | No (overloading) | Yes (usually) |
| Flexibility | Fixed set of overloads | New derived types later |
| Speed | Often faster / inline-friendly | Small virtual-call cost |
| Example call | add(2,3) vs add(2.5,3.1) | shape->draw() |
Compile-time — function overloading
- Same function **name**, different **parameter lists** (type, count, or order).
- Compiler picks the best match from the call arguments.
- Return type alone is **not** enough to overload.
- **Real-life:** one “print” desk — print receipt, print ticket, print report (different forms).
#include <iostream>
using namespace std;
void print(int x) {
cout << "int: " << x << "\n";
}
void print(double x) {
cout << "double: " << x << "\n";
}
void print(const char* s) {
cout << "string: " << s << "\n";
}
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int main() {
print(10); // int overload
print(3.14); // double overload
print("Hello"); // const char* overload
cout << add(2, 3) << "\n"; // 5
cout << add(2.5, 3.1) << "\n"; // 5.6
return 0;
} Compile-time — operator overloading
- Give operators (
+,<<,==, …) meaning for your types. - Resolved at compile time from operand types — still static polymorphism.
- Keep behaviour intuitive (e.g.
+should feel like addition/combination).
#include <iostream>
using namespace std;
class Complex {
double re, im;
public:
Complex(double r = 0, double i = 0) : re(r), im(i) {}
Complex operator+(const Complex& o) const {
return Complex(re + o.re, im + o.im);
}
void show() const { cout << re << " + " << im << "i\n"; }
};
int main() {
Complex a(1, 2), b(3, 4);
Complex c = a + b; // operator+ overload
c.show(); // 4 + 6i
return 0;
} Compile-time — templates (overview)
- Function/class **templates** generate type-specific code at compile time.
- One template definition → many concrete forms (
max,max). - Powerful static polymorphism used heavily in the STL.
#include <iostream>
using namespace std;
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << maximum(10, 20) << "\n"; // int
cout << maximum(2.5, 1.5) << "\n"; // double
cout << maximum('a', 'z') << "\n"; // char
return 0;
} Runtime — virtual functions
- Declare
virtualin the base; override in derived classes. - Call through a **base pointer or reference** — the **actual object type** decides which override runs.
- This is **late binding** (dynamic dispatch).
- Prefer
override; give polymorphic bases a **virtual destructor**. - **Real-life:** Shape.draw() — Circle, Rectangle, Triangle each draw differently.
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() const { cout << "Shape\n"; }
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override { cout << "Circle\n"; }
};
class Rectangle : public Shape {
public:
void draw() const override { cout << "Rectangle\n"; }
};
int main() {
Circle c;
Rectangle r;
Shape* p;
p = &c; p->draw(); // Circle (runtime)
p = &r; p->draw(); // Rectangle
return 0;
} Runtime — same call, different objects
- A function that takes
Shape&orShape*can draw any concrete shape. - New shapes can be added later without changing that function — classic runtime polymorphism.
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Triangle : public Shape {
public:
void draw() const override { cout << "Triangle\n"; }
};
class Line : public Shape {
public:
void draw() const override { cout << "Line\n"; }
};
void render(const Shape& s) {
s.draw(); // runtime choice
}
int main() {
Triangle t;
Line L;
render(t);
render(L);
return 0;
} Function overloading vs overriding
- Easy to mix up in interviews — keep the difference clear.
| Aspect | Overloading (compile-time) | Overriding (runtime) |
|---|---|---|
| Where | Same scope (usually) | Base + derived classes |
| Signature | Must differ (params) | Must match (virtual) |
| Keyword | None special | virtual / override |
| Binding | Compile time | Run time (via base ptr/ref) |
| Inheritance needed? | No | Yes |
Key points to remember
- Polymorphism = many forms of one interface.
- Compile-time: function overloading, operator overloading, templates.
- Runtime: virtual functions through base pointers/references.
- Overloading ≠ overriding.
- Runtime polymorphism needs inheritance + virtual.
- Use compile-time when types are known early; runtime when behaviour varies by dynamic type.
Advanced — Polymorphism
When to choose which kind
- Prefer **overloading/templates** for utilities on known types (math, containers, operators).
- Prefer **virtuals** for open hierarchies (plugins, UI widgets, game entities).
- Mixing both is normal: overloaded helpers + a virtual
draw().
| Situation | Prefer |
|---|---|
| Same name, different arg types | Function overloading |
| Natural operator for a class | Operator overloading |
| Generic algorithm for many types | Templates |
| Base API, many derived behaviours | Virtual functions |
| Hot inner loop, type known | Static polymorphism / templates |
Name hiding vs overriding
- If the derived function does **not** match a base virtual (e.g. missing
const), it may **hide** instead of override. - Always use
overrideto catch this. - Non-virtual “same name” in derived **hides** the base function for that name — different from polymorphic override.
class Base {
public:
virtual void f() const {}
};
class Bad : public Base {
public:
void f() {} // missing const — does NOT override (hides / wrong)
};
class Good : public Base {
public:
void f() const override {} // correct override
}; Operator overloading guidelines
- Overload only when the meaning is obvious.
- Prefer non-member (often
friend) for symmetric operators like<<. - Do not overload
&&,||,,in modern C++ — short-circuit / sequencing surprises. - Still compile-time polymorphism: the operator call is fixed from static types.
Templates vs virtuals
- Templates: code generated per type; no common base required; hard to store “any T” in one container without type erasure.
- Virtuals: one binary interface; easy
vector; small runtime cost. - Advanced patterns (CRTP) get static polymorphism that looks like inheritance — optional later topic.
Runtime checklist
- Mark the base function
virtual. - Override with matching signature +
override. - Call via
Base*orBase&to see dynamic dispatch. - Use a **virtual destructor** in the polymorphic base.
- Details deepened in the Virtual Functions lesson.
Real-life use cases
- Compile-time:
std::cout << x, overloaded constructors, STL algorithms on many types. - Runtime: document renderers, payment gateways, media players, sensor drivers.
- Banking:
Transaction::execute()virtual — Deposit / Withdraw / Transfer.
Tricky Interview Points
Q1) What is polymorphism in C++?
Answer The ability to use one interface in many forms — mainly compile-time (overloading/templates) and runtime (virtual functions).
Q2) What are the types of polymorphism in C++?
Answer Compile-time (static) polymorphism and runtime (dynamic) polymorphism.
Q3) Give examples of compile-time polymorphism.
Answer Function overloading, operator overloading, and templates.
Q4) Give an example of runtime polymorphism.
Answer A virtual draw() in Shape overridden by Circle and Rectangle, called through Shape*.
Q5) Difference between compile-time and runtime polymorphism?
Answer Compile-time binding is decided by the compiler (overloads/templates). Runtime binding uses virtual dispatch based on the actual object type.
Q6) Overloading vs overriding?
Answer Overloading: same name, different parameters, same scope, compile-time. Overriding: same signature in derived, virtual, runtime.
Q7) Is operator overloading compile-time or runtime?
Answer Compile-time — the operator function is selected from operand types at compile time.
Q8) Can we achieve runtime polymorphism without virtual?
Answer Not in the usual C++ OOP sense. Without virtual, base-pointer calls use static binding to the base version.
Q9) Why are templates called static polymorphism?
Answer The compiler generates and binds concrete functions/classes for each type at compile time.
Q10) What is needed for runtime polymorphism?
Answer Inheritance, at least one virtual function, and typically a base pointer or reference to a derived object.
Q11) Can constructors be virtual?
Answer No. Destructors of polymorphic bases should be virtual; constructors cannot.
Q12) Real-life example of polymorphism?
Answer A Pay action that works for UPI/card/wallet (runtime), or + meaning numeric add vs string concat (compile-time).
Simple Programs
Compile-time — function overloading
#include <iostream>
using namespace std;
void show(int x) { cout << "int " << x << "\n"; }
void show(double x) { cout << "double " << x << "\n"; }
void show(int x, int y) { cout << "pair " << x << "," << y << "\n"; }
int main() {
show(5);
show(2.5);
show(1, 2);
return 0;
} Compile-time — operator overloading
#include <iostream>
using namespace std;
class Point {
int x, y;
public:
Point(int a = 0, int b = 0) : x(a), y(b) {}
Point operator+(const Point& o) const {
return Point(x + o.x, y + o.y);
}
void print() const { cout << "(" << x << "," << y << ")\n"; }
};
int main() {
Point a(1, 2), b(3, 4);
Point c = a + b;
c.print(); // (4,6)
return 0;
} Compile-time — function template
#include <iostream>
using namespace std;
template <typename T>
void swapValues(T& a, T& b) {
T t = a; a = b; b = t;
}
int main() {
int i = 1, j = 2;
swapValues(i, j);
cout << i << " " << j << "\n";
double x = 1.5, y = 3.5;
swapValues(x, y);
cout << x << " " << y << "\n";
return 0;
} Runtime — virtual draw()
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() const { cout << "Shape\n"; }
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override { cout << "Circle\n"; }
};
class Square : public Shape {
public:
void draw() const override { cout << "Square\n"; }
};
int main() {
Circle c; Square s;
Shape* shapes[2] = { &c, &s };
for (Shape* p : shapes) p->draw();
return 0;
} Runtime — payment hierarchy
#include <iostream>
using namespace std;
class Payment {
public:
virtual void pay(double amount) const = 0;
virtual ~Payment() = default;
};
class UpiPay : public Payment {
public:
void pay(double amount) const override {
cout << "UPI paid " << amount << "\n";
}
};
class CardPay : public Payment {
public:
void pay(double amount) const override {
cout << "Card paid " << amount << "\n";
}
};
void checkout(const Payment& p, double amt) {
p.pay(amt); // runtime polymorphism
}
int main() {
UpiPay u; CardPay c;
checkout(u, 100);
checkout(c, 250);
return 0;
} Side-by-side — overload + virtual
#include <iostream>
using namespace std;
// Compile-time
int area(int side) { return side * side; }
int area(int l, int w) { return l * w; }
// Runtime
class Animal {
public:
virtual void speak() const { cout << "???\n"; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override { cout << "Bark\n"; }
};
int main() {
cout << area(4) << "\n"; // 16
cout << area(3, 5) << "\n"; // 15
Dog d;
Animal* p = &d;
p->speak(); // Bark
return 0;
} Problem Solving Tricks
- Polymorphism = many forms of one interface.
- Compile-time: overloading + templates; runtime: virtual.
- Overloading → different params; overriding → same signature + virtual.
- operator+ is still compile-time polymorphism.
- Runtime needs Base* / Base& to a derived object.
- Always use override on intended overrides.
- Polymorphic base ⇒ virtual destructor.
- Return type alone cannot overload functions.
- Templates = static polymorphism used by the STL.
- If types are fixed and known, prefer overloads/templates for speed.
- If behaviour must vary by dynamic type, use virtuals.
- Practice one overload example and one virtual example for interviews.
Practice Programs List
- Write three overloads of display() for int, double, and string.
- Overload operator+ for a Complex or Point class.
- Write a template max(a,b) and call it with int and double.
- Implement Shape with virtual draw(); Circle and Rectangle overrides.
- Pass Shape& to a render() function; call with different shapes.
- Build Payment with pay(); UpiPay and CardPay; checkout via base reference.
- Show a non-virtual base call vs virtual call side by side.
- Create a table in comments: compile-time vs runtime with your own examples.
- Intentionally mismatch an override signature; fix it using override.
- Store several Animal* and call speak() in a loop.
- Overload a constructor set (default / parameterized) — note it is compile-time.
- Explain when you would pick templates instead of a virtual hierarchy.