04-polymorphism.md

Module 04: Polymorphism

Goal: Master compile-time and runtime polymorphism, virtual functions, vtable, operator overloading, and RTTI. Time: 2 days of focused study Prerequisites: Module 01-03


Table of Contents

  1. What is Polymorphism?
  2. Compile-Time Polymorphism
  3. Runtime Polymorphism
  4. Virtual Functions — Under the Hood
  5. Pure Virtual Functions & Abstract Classes
  6. Operator Overloading
  7. RTTI — Runtime Type Information
  8. Compile-Time vs Runtime Polymorphism
  9. Covariant Return Types
  10. Common Mistakes
  11. Practice Problems
  12. Interview Questions

1. What is Polymorphism?

Polymorphism = "many forms." The same interface behaves differently depending on the underlying type.

Real-World Analogy: A REMOTE CONTROL "Press Play" (same interface): On a DVD player → plays a movie On a music player → plays a song On a game console → resumes the game Same button, different behavior depending on the device. Two Types: COMPILE-TIME (Static / Early Binding): Decision made at compile time. → Function overloading → Operator overloading → Templates RUNTIME (Dynamic / Late Binding): Decision made at runtime. → Virtual functions → Function overriding (via base pointer/reference)

2. Compile-Time Polymorphism

Function Overloading

Same function name, different parameter lists. Resolved at compile time.

class Printer { public: // Same name, different parameter types void print(int val) { cout << "Integer: " << val << endl; } void print(double val) { cout << "Double: " << val << endl; } void print(const string& val) { cout << "String: " << val << endl; } void print(int val, int base) { cout << "Int in base " << base << ": "; // ... print in given base } }; int main() { Printer p; p.print(42); // Calls print(int) p.print(3.14); // Calls print(double) p.print("hello"s); // Calls print(const string&) p.print(255, 16); // Calls print(int, int) // The compiler decides WHICH function to call based on the arguments. // This is resolved at COMPILE TIME (static dispatch). }

Overloading Rules

CAN overload based on: ✅ Number of parameters ✅ Type of parameters const/non-const (for member functions) ✅ lvalue/rvalue reference CANNOT overload based on: ❌ Return type only ❌ Default arguments that make calls ambiguous
// ❌ These are AMBIGUOUS: int getVal() { return 1; } double getVal() { return 1.0; } // Error: functions only differing by return type can't be overloaded // ❌ AMBIGUOUS with default arguments: void foo(int x, int y = 10) {} void foo(int x) {} // foo(5); — Which one? Ambiguous!

Templates (Generic Programming)

// Write code ONCE that works with ANY type template <typename T> T getMax(T a, T b) { return (a > b) ? a : b; } int main() { cout << getMax(3, 7) << endl; // int version generated at compile time cout << getMax(3.14, 2.72) << endl; // double version generated cout << getMax('a', 'z') << endl; // char version generated } // Class template template <typename T> class Stack { vector<T> data; public: void push(const T& val) { data.push_back(val); } T pop() { if (data.empty()) throw runtime_error("Stack empty!"); T val = data.back(); data.pop_back(); return val; } bool empty() const { return data.empty(); } size_t size() const { return data.size(); } }; int main() { Stack<int> intStack; Stack<string> strStack; intStack.push(42); strStack.push("hello"); }

3. Runtime Polymorphism

Runtime polymorphism is achieved through virtual functions and base class pointers/references.

class Shape { public: virtual double area() const = 0; virtual string name() const = 0; virtual ~Shape() = default; }; class Circle : public Shape { double radius; public: Circle(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; } string 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; } string name() const override { return "Rectangle"; } }; class Triangle : public Shape { double base, height; public: Triangle(double b, double h) : base(b), height(h) {} double area() const override { return 0.5 * base * height; } string name() const override { return "Triangle"; } }; // THIS FUNCTION DOESN'T KNOW THE ACTUAL TYPE! // It works with ANY shape — current or FUTURE shapes! void printShapeInfo(const Shape& shape) { cout << shape.name() << ": area = " << shape.area() << endl; } int main() { Circle c(5); Rectangle r(4, 6); Triangle t(3, 8); // All treated as Shape — the CORRECT method is called at RUNTIME printShapeInfo(c); // Circle: area = 78.5398 printShapeInfo(r); // Rectangle: area = 24 printShapeInfo(t); // Triangle: area = 12 // Polymorphic collection vector<unique_ptr<Shape>> shapes; shapes.push_back(make_unique<Circle>(10)); shapes.push_back(make_unique<Rectangle>(5, 3)); shapes.push_back(make_unique<Triangle>(6, 4)); double totalArea = 0; for (const auto& shape : shapes) { totalArea += shape->area(); // Calls the right area() for each } cout << "Total area: " << totalArea << endl; }

4. Virtual Functions — Under the Hood

The vtable (Virtual Table)

When a class has virtual functions, the compiler creates a VTABLE: a lookup table of function pointers for that class. Each OBJECT gets a hidden pointer called VPTR (virtual pointer) that points to its class's vtable. class Animal { virtual void speak() {} // slot 0 virtual void eat() {} // slot 1 }; class Dog : public Animal { void speak() override {} // replaces slot 0 // eat() inherited // keeps slot 1 virtual void fetch() {} // slot 2 (new) }; class Cat : public Animal { void speak() override {} // replaces slot 0 void eat() override {} // replaces slot 1 }; VTABLE for Animal: [0]Animal::speak() [1]Animal::eat() VTABLE for Dog: [0]Dog::speak() ← overridden! [1]Animal::eat() ← inherited [2]Dog::fetch()new VTABLE for Cat: [0]Cat::speak() ← overridden! [1]Cat::eat() ← overridden! Object layout: Animal obj: [vptr → Animal_vtable | other_data] Dog obj: [vptr → Dog_vtable | Animal_data | Dog_data] Cat obj: [vptr → Cat_vtable | Animal_data | Cat_data]

How Virtual Dispatch Works

Animal* ptr = new Dog(); ptr->speak(); // Compiler generates (pseudocode): // 1. Get the vptr from the object: vptr = ptr->__vptr // 2. Look up the function in vtable: func = vptr[0] (speak is slot 0) // 3. Call the function: func(ptr) // // Since ptr points to a Dog, __vptr points to Dog_vtable, // so vptr[0] is Dog::speak(), not Animal::speak()! // This is "dynamic dispatch" — the function call is DISPATCHED // at RUNTIME based on the actual object type.

Performance Cost of Virtual Functions

Virtual function call: 1. Load vptr from object (memory access) 2. Index into vtable (memory access) 3. Call through function pointer (indirect call) Total: ~2 extra memory accesses vs direct call Non-virtual function call: 1. Call function directly (address known at compile time) Cost: ~1-5 nanoseconds per virtual call overhead Usually negligible unless in a very tight loop. Memory overhead: - One vptr per object (~8 bytes on 64-bit) - One vtable per CLASS (not per object — shared)

5. Pure Virtual Functions & Abstract Classes

class PaymentProcessor { public: // Pure virtual — MUST be implemented by derived classes virtual bool processPayment(double amount) = 0; virtual bool refund(double amount) = 0; virtual string getProviderName() const = 0; // Non-pure virtual — has default implementation virtual void logTransaction(double amount, bool success) { cout << "[" << getProviderName() << "] $" << amount << (success ? " SUCCESS" : " FAILED") << endl; } virtual ~PaymentProcessor() = default; }; class StripeProcessor : public PaymentProcessor { public: bool processPayment(double amount) override { // Call Stripe API... bool success = true; logTransaction(amount, success); return success; } bool refund(double amount) override { // Call Stripe refund API... return true; } string getProviderName() const override { return "Stripe"; } }; class PayPalProcessor : public PaymentProcessor { public: bool processPayment(double amount) override { // Call PayPal API... bool success = true; logTransaction(amount, success); return success; } bool refund(double amount) override { return true; } string getProviderName() const override { return "PayPal"; } }; // Works with ANY payment processor — now or in the future! void checkout(PaymentProcessor& processor, double total) { if (processor.processPayment(total)) { cout << "Payment successful!" << endl; } }

6. Operator Overloading

Operator overloading lets you define how operators (+, -, <<, ==, etc.) work with your custom types.

Basic Operators

class Vector2D { double x, y; public: Vector2D(double x = 0, double y = 0) : x(x), y(y) {} // Addition: vec1 + vec2 Vector2D operator+(const Vector2D& other) const { return Vector2D(x + other.x, y + other.y); } // Subtraction: vec1 - vec2 Vector2D operator-(const Vector2D& other) const { return Vector2D(x - other.x, y - other.y); } // Scalar multiplication: vec * 3.0 Vector2D operator*(double scalar) const { return Vector2D(x * scalar, y * scalar); } // Equality: vec1 == vec2 bool operator==(const Vector2D& other) const { return x == other.x && y == other.y; } bool operator!=(const Vector2D& other) const { return !(*this == other); } // Negation: -vec Vector2D operator-() const { return Vector2D(-x, -y); } // Compound assignment: vec1 += vec2 Vector2D& operator+=(const Vector2D& other) { x += other.x; y += other.y; return *this; } // Subscript: vec[0] = x, vec[1] = y double& operator[](int index) { if (index == 0) return x; if (index == 1) return y; throw out_of_range("Index must be 0 or 1"); } // Stream output (must be friend because left operand is ostream) friend ostream& operator<<(ostream& os, const Vector2D& v) { os << "(" << v.x << ", " << v.y << ")"; return os; } // Scalar * vec (reverse order — must be friend/non-member) friend Vector2D operator*(double scalar, const Vector2D& v) { return v * scalar; } }; int main() { Vector2D a(3, 4), b(1, 2); Vector2D c = a + b; // (4, 6) Vector2D d = a * 2.0; // (6, 8) Vector2D e = 3.0 * a; // (9, 12) bool eq = (a == b); // false cout << c << endl; // (4, 6) cout << -a << endl; // (-3, -4) a += b; cout << a << endl; // (4, 6) cout << a[0] << endl; // 4 }

Comparison Operators (C++20 Spaceship)

#include <compare> class Version { int major, minor, patch; public: Version(int ma, int mi, int pa) : major(ma), minor(mi), patch(pa) {} // C++20: one operator generates all comparisons! auto operator<=>(const Version& other) const = default; // Generates: ==, !=, <, >, <=, >= }; // Pre-C++20, you had to write each one manually

Increment/Decrement

class Counter { int value; public: Counter(int v = 0) : value(v) {} // Pre-increment: ++counter (returns modified object) Counter& operator++() { value++; return *this; } // Post-increment: counter++ (returns old value) Counter operator++(int) { // 'int' is a dummy parameter to distinguish Counter old = *this; value++; return old; } friend ostream& operator<<(ostream& os, const Counter& c) { return os << c.value; } }; int main() { Counter c(5); cout << ++c << endl; // 6 (increments, then returns) cout << c++ << endl; // 6 (returns, then increments) cout << c << endl; // 7 }

Function Call Operator (Functors)

// A class that can be "called" like a function class Multiplier { int factor; public: Multiplier(int f) : factor(f) {} int operator()(int x) const { return x * factor; } }; int main() { Multiplier triple(3); Multiplier doubler(2); cout << triple(10) << endl; // 30 cout << doubler(10) << endl; // 20 // Functors can be used with STL algorithms: vector<int> nums = {1, 2, 3, 4, 5}; transform(nums.begin(), nums.end(), nums.begin(), triple); // nums = {3, 6, 9, 12, 15} }

Conversion Operators

class Fraction { int num, den; public: Fraction(int n, int d) : num(n), den(d) {} // Implicit conversion to double operator double() const { return static_cast<double>(num) / den; } // Explicit conversion to bool explicit operator bool() const { return num != 0; } }; int main() { Fraction f(3, 4); double d = f; // 0.75 (implicit conversion) // bool b = f; // ❌ Error if explicit if (f) { ... } // ✅ explicit operator bool works in conditions }

What CAN'T Be Overloaded

CANNOT overload: :: (scope resolution) . (member access) .* (member pointer access) ?: (ternary) sizeof typeid CANNOT create NEW operators (no ** or @@) CANNOT change operator precedence or associativity CANNOT change the number of operands (+ is always binary or unary)

7. RTTI

Runtime Type Information — checking the actual type of an object at runtime.

#include <typeinfo> class Animal { public: virtual ~Animal() = default; // MUST have virtual function for RTTI! }; class Dog : public Animal {}; class Cat : public Animal {}; int main() { Animal* a = new Dog(); // ---- typeid ---- cout << typeid(*a).name() << endl; // "Dog" (implementation-defined) if (typeid(*a) == typeid(Dog)) { cout << "It's a dog!" << endl; } // ---- dynamic_cast ---- // Safely cast base to derived (returns nullptr if wrong type) Dog* d = dynamic_cast<Dog*>(a); if (d) { cout << "Successfully cast to Dog" << endl; // Use d as Dog* } Cat* c = dynamic_cast<Cat*>(a); if (c) { cout << "It's a cat" << endl; } else { cout << "NOT a cat" << endl; // ← This one } // dynamic_cast with references (throws bad_cast on failure) try { Cat& catRef = dynamic_cast<Cat&>(*a); } catch (const bad_cast& e) { cout << "Cast failed: " << e.what() << endl; } delete a; } /* dynamic_cast vs static_cast: static_cast: - Compile-time check only - No runtime overhead - UNSAFE for downcasting (base→derived) — undefined behavior if wrong type! dynamic_cast: - Runtime check (uses RTTI) - Slight performance overhead - SAFE — returns nullptr or throws bad_cast if wrong type - Requires at least one virtual function in the base class PREFER dynamic_cast for downcasting. But BETTER: avoid downcasting entirely — use polymorphism instead. */

8. Compile-Time vs Runtime

Feature │ Compile-Time │ Runtime ──────────────────┼────────────────────────┼──────────────────────── Also called │ Static, Early binding │ Dynamic, Late binding Resolved at │ Compile time │ Runtime Mechanism │ Overloading, templates │ Virtual functions Speed │ Faster (no indirection)│ Slight overhead (vtable) Flexibility │ Types known at compile │ Types can vary at runtime Keyword │ (none needed)virtual Errors │ Caught at compile time │ Caught at runtime Example │ add(int) vs add(double)│ shape->area() calls the right one WHEN TO USE WHICH: Compile-time: ✅ Performance-critical code ✅ Types are known at compile time ✅ Generic programming (templates) Runtime: ✅ Heterogeneous collections (vector<Shape*>) ✅ Plugin architectures (types not known at compile time) ✅ Framework/library code (users define new types)

9. Covariant Return Types

class Animal { public: virtual Animal* clone() const { return new Animal(*this); } virtual ~Animal() = default; }; class Dog : public Animal { public: // Return type can be a DERIVED type of the base's return type! Dog* clone() const override { // Returns Dog*, not Animal* return new Dog(*this); } }; int main() { Dog d; Dog* copy = d.clone(); // Returns Dog* directly (no casting needed) Animal* a = &d; Animal* aCopy = a->clone(); // Returns Animal* (but actually a Dog*) delete copy; delete aCopy; } // Covariant return types let derived classes return a more specific type // while still satisfying the base class interface.

10. Common Mistakes

// ❌ MISTAKE 1: Forgetting `virtual` for polymorphism class Base { public: void speak() { cout << "Base" << endl; } // NOT virtual! }; class Derived : public Base { public: void speak() { cout << "Derived" << endl; } }; Base* p = new Derived(); p->speak(); // "Base" ← Wrong! Not virtual, so no dynamic dispatch. // ❌ MISTAKE 2: Calling virtual functions in constructor/destructor class Base { public: Base() { init(); // Calls Base::init(), NOT Derived::init()! } virtual void init() { cout << "Base init" << endl; } }; class Derived : public Base { public: void init() override { cout << "Derived init" << endl; } }; // During Base construction, the object IS a Base (Derived part not yet constructed) // ❌ MISTAKE 3: Overloading when you mean to override class Base { virtual void process(int x) {} }; class Derived : public Base { void process(double x) {} // This is OVERLOADING (different type), not overriding! // void process(int x) override {} ✅ This is overriding }; // ❌ MISTAKE 4: Overloading operators inconsistently // If you overload ==, also overload != // If you overload <, also overload >, <=, >= // Or use C++20 <=> operator

11. Practice Problems

Problem 1: Polymorphic Calculator

class Operation { public: virtual double calculate(double a, double b) const = 0; virtual string symbol() const = 0; virtual ~Operation() = default; }; class Add : public Operation { public: double calculate(double a, double b) const override { return a + b; } string symbol() const override { return "+"; } }; class Subtract : public Operation { public: double calculate(double a, double b) const override { return a - b; } string symbol() const override { return "-"; } }; class Multiply : public Operation { public: double calculate(double a, double b) const override { return a * b; } string symbol() const override { return "*"; } }; class Divide : public Operation { public: double calculate(double a, double b) const override { if (b == 0) throw runtime_error("Division by zero!"); return a / b; } string symbol() const override { return "/"; } }; // Usage: void compute(const Operation& op, double a, double b) { cout << a << " " << op.symbol() << " " << b << " = " << op.calculate(a, b) << endl; } int main() { Add add; Multiply mul; Divide div; compute(add, 10, 3); // 10 + 3 = 13 compute(mul, 4, 5); // 4 * 5 = 20 compute(div, 10, 3); // 10 / 3 = 3.33333 }

Problem 2: Smart Matrix Class with Operators

class Matrix { vector<vector<double>> data; int rows, cols; public: Matrix(int r, int c) : rows(r), cols(c), data(r, vector<double>(c, 0)) {} double& operator()(int r, int c) { return data[r][c]; } double operator()(int r, int c) const { return data[r][c]; } Matrix operator+(const Matrix& other) const { if (rows != other.rows || cols != other.cols) throw invalid_argument("Matrix size mismatch!"); Matrix result(rows, cols); for (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) result(i, j) = data[i][j] + other(i, j); return result; } bool operator==(const Matrix& other) const { if (rows != other.rows || cols != other.cols) return false; for (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) if (data[i][j] != other(i, j)) return false; return true; } friend ostream& operator<<(ostream& os, const Matrix& m) { for (int i = 0; i < m.rows; i++) { for (int j = 0; j < m.cols; j++) os << m(i, j) << "\t"; os << "\n"; } return os; } };

12. Interview Questions

Q1: What is polymorphism? Explain its types.

Answer: Polymorphism means "many forms" — the same interface behaves differently depending on the type. Two types: (1) Compile-time (static) — resolved at compile time via function overloading, operator overloading, and templates. (2) Runtime (dynamic) — resolved at runtime via virtual functions and base pointers/references. The compiler generates vtable lookups for dynamic dispatch.

Q2: What is a virtual function? How does it work internally?

Answer: A virtual function enables runtime polymorphism. When called through a base pointer, the actual derived version is called. Internally: the compiler creates a vtable (array of function pointers) per class, and each object gets a vptr (pointer to its class's vtable). Virtual dispatch: load vptr → index vtable → call function pointer. Overhead: ~8 bytes per object (vptr) + ~2 memory accesses per call.

Q3: What is the vtable and vptr?

Answer: The vtable (virtual table) is a compile-time generated array of function pointers, one per class with virtual functions. It maps each virtual function to its implementation for that class. The vptr (virtual pointer) is a hidden member in each object that points to its class's vtable. When you override a function, the derived class's vtable entry is updated to point to the new implementation.

Q4: Can constructors be virtual? Can destructors?

Answer: Constructors CANNOT be virtual — the vtable doesn't exist during construction. Destructors CAN and SHOULD be virtual when the class is used as a base class. Without a virtual destructor, deleting a derived object through a base pointer skips the derived destructor, causing resource leaks.

Q5: What is function overloading vs overriding?

Answer: Overloading: same function name, different parameters, in the SAME class. Resolved at compile time. Overriding: same function signature in derived class replaces base class virtual function. Resolved at runtime. Overloading is compile-time polymorphism; overriding is runtime polymorphism.

Q6: What is operator overloading? What operators can't be overloaded?

Answer: Operator overloading defines custom behavior for operators with user-defined types. Can't overload: ::, ., .*, ?:, sizeof, typeid. Can't create new operators or change precedence. Implement as member for unary and compound assignment; as friend/non-member for binary operators where the left operand might not be your type (like cout << obj).

Q7: What is dynamic_cast? When do you use it?

Answer: dynamic_cast safely converts base pointers/references to derived types at runtime using RTTI. Returns nullptr (pointer) or throws bad_cast (reference) on failure. Requires at least one virtual function. Use when you need type-specific behavior that can't be achieved through polymorphism. But prefer virtual functions over dynamic_cast — it often signals a design problem.

Q8: Why are virtual functions slower than non-virtual?

Answer: Virtual calls require two extra memory accesses (load vptr, index vtable) and an indirect function call (through pointer). They also prevent inlining. Non-virtual calls are direct (address known at compile time) and can be inlined. The overhead is ~1-5ns per call — negligible except in extremely performance-critical tight loops.

Q9: Can you call a virtual function in a constructor?

Answer: Technically yes, but it won't behave polymorphically. During base class construction, the vptr points to the base class vtable (the derived class doesn't exist yet). So calling a virtual function in a Base constructor calls Base's version, even if Derived overrides it. This is a common source of bugs.

Q10: What is a functor?

Answer: A functor is a class that overloads operator(), making objects of that class callable like functions. Advantages over function pointers: they can hold state (member variables), they're faster (compiler can inline), and they work naturally with STL algorithms. Lambdas in C++11 are essentially compiler-generated functors.

Q11: Explain the difference between static_cast and dynamic_cast.

Answer: static_cast performs compile-time type checking only — no runtime check, undefined behavior if the actual type is wrong. dynamic_cast performs runtime type checking using RTTI — returns nullptr/throws if the cast is invalid. Use static_cast for known-safe conversions (int→double, upcasting). Use dynamic_cast for downcasting when you're not sure of the actual type.

Q12: What are covariant return types?

Answer: When overriding a virtual function, the return type can be a derived type of the base function's return type. If Base::clone() returns Base*, Derived::clone() can return Derived*. This avoids unnecessary casting when the function is called on a known derived type.


Next Module: 05 - SOLID Principles — The five fundamental design principles for clean, maintainable OOP code.