Skip to main content

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.

L1

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.
IdeaMeaningC++ tools
One nameSame symbol / call styleadd, operator+, draw
Many formsDifferent implementationsOverloads / overrides
Compile-timeResolved before the program runsOverloading, templates
RuntimeResolved while the program runsVirtual 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.
TypeAlso calledMain techniquesBinding
Compile-timeStatic polymorphismFunction overloading, operator overloading, templatesEarly / static
RuntimeDynamic polymorphismVirtual functions + inheritanceLate / 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.
AspectCompile-timeRuntime
When decidedCompile timeRun time
Typical toolsOverloading, templatesvirtual + base pointer/ref
Needs inheritance?No (overloading)Yes (usually)
FlexibilityFixed set of overloadsNew derived types later
SpeedOften faster / inline-friendlySmall virtual-call cost
Example calladd(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 virtual in 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& or Shape* 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.
AspectOverloading (compile-time)Overriding (runtime)
WhereSame scope (usually)Base + derived classes
SignatureMust differ (params)Must match (virtual)
KeywordNone specialvirtual / override
BindingCompile timeRun time (via base ptr/ref)
Inheritance needed?NoYes

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.
L2

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().
SituationPrefer
Same name, different arg typesFunction overloading
Natural operator for a classOperator overloading
Generic algorithm for many typesTemplates
Base API, many derived behavioursVirtual functions
Hot inner loop, type knownStatic 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 override to 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* or Base& 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.
L3

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).

L4

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;
}
L5

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.
L6

Practice Programs List

  1. Write three overloads of display() for int, double, and string.
  2. Overload operator+ for a Complex or Point class.
  3. Write a template max(a,b) and call it with int and double.
  4. Implement Shape with virtual draw(); Circle and Rectangle overrides.
  5. Pass Shape& to a render() function; call with different shapes.
  6. Build Payment with pay(); UpiPay and CardPay; checkout via base reference.
  7. Show a non-virtual base call vs virtual call side by side.
  8. Create a table in comments: compile-time vs runtime with your own examples.
  9. Intentionally mismatch an override signature; fix it using override.
  10. Store several Animal* and call speak() in a loop.
  11. Overload a constructor set (default / parameterized) — note it is compile-time.
  12. Explain when you would pick templates instead of a virtual hierarchy.