oops-02-polymorphism-deep-dive.md

Polymorphism Deep Dive — HIGHEST PRIORITY

This is the most-tested OOP topic at MathWorks. Interviewers ask theory AND live coding on this. Spoken answer target: under 45 seconds each


1. Compile-Time Polymorphism — Function Overloading

Definition:

Multiple functions share the same name but differ in parameter type or count. The compiler decides which to call at compile time — no runtime cost.

Key Point:

  • Also called static dispatch or early binding
  • You CANNOT overload by return type alone
  • Default arguments can cause ambiguity — watch out
class Calculator { public: int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } int add(int a, int b, int c) { return a + b + c; } // double add(int a, int b) { return ...; } // ERROR — return type differs only }; int main() { Calculator c; c.add(1, 2); // Calls add(int, int) c.add(1.5, 2.5); // Calls add(double, double) c.add(1, 2, 3); // Calls add(int, int, int) }

Java equivalent:

class Calculator { int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } }

Likely follow-up:

"Can you overload based on return type?" No. The compiler cannot distinguish calls like int x = add(1,2) vs double x = add(1,2) because the return type is inferred from context — it's ambiguous.

Common mistake: Adding a default argument that makes two overloads ambiguous:

void foo(int x, int y = 10) {} void foo(int x) {} foo(5); // ERROR! Compiler cannot tell which to call.

2. Compile-Time Polymorphism — Operator Overloading

Definition:

Defining custom behavior for C++ operators (like +, -, ==, <<) when used with user-defined types. Resolved at compile time.

Key Point:

  • Makes your class behave like a built-in type
  • << and commutative binary operators must be friend functions (left operand is not your class)
  • Cannot overload: ::, ., .*, ?:, sizeof, typeid
  • Pre-increment (++x) returns reference; Post-increment (x++) returns copy
class Vector2D { double x, y; public: Vector2D(double x = 0, double y = 0) : x(x), y(y) {} // Binary +: member function (left operand is *this) Vector2D operator+(const Vector2D& other) const { return Vector2D(x + other.x, y + other.y); } // == operator bool operator==(const Vector2D& other) const { return x == other.x && y == other.y; } // Pre-increment: returns modified object Vector2D& operator++() { x++; y++; return *this; } // Post-increment: returns OLD value (takes dummy int param) Vector2D operator++(int) { Vector2D old = *this; x++; y++; return old; } // << must be friend (ostream is left operand, not Vector2D) friend ostream& operator<<(ostream& os, const Vector2D& v) { os << "(" << v.x << ", " << v.y << ")"; return os; } // 3.0 * vec — scalar on LEFT requires friend/non-member friend Vector2D operator*(double s, const Vector2D& v) { return Vector2D(s * v.x, s * v.y); } }; int main() { Vector2D a(3, 4), b(1, 2); Vector2D c = a + b; // (4, 6) Vector2D d = 2.0 * a; // (6, 8) — uses friend operator* cout << c << endl; // (4, 6) ++a; // a = (4, 5) Vector2D old = a++; // old = (4, 5), a = (5, 6) }

Likely follow-up:

"Why does operator<< have to be a friend?" Because the left operand of << is ostream, not your class. Member functions require this (your object) to be the left operand. Since we cannot modify ostream, we make it a free function — and grant it friend access to our private data.

Common mistake: Overloading == but forgetting !=, or overloading < without the others. In C++20, define the spaceship operator <=> and get all comparisons for free.


3. Runtime Polymorphism — Virtual Functions

Definition:

A virtual function is a member function declared with virtual, enabling the correct derived-class version to be called through a base-class pointer/reference at runtime.

Key Point — HOW IT WORKS (vtable/vptr):

  • Compiler creates a vtable (array of function pointers) per class that has virtual functions
  • Each object gets a hidden vptr pointing to its class's vtable
  • When you call a virtual function through a base pointer: CPU reads vptr → looks up function in vtable → calls it
  • This is dynamic dispatch — the actual type of the object determines which function runs
class Animal { public: virtual void speak() const { cout << "..."; } virtual ~Animal() {} // Virtual destructor is CRITICAL }; class Dog : public Animal { public: void speak() const override { cout << "Woof!"; } }; class Cat : public Animal { public: void speak() const override { cout << "Meow!"; } }; int main() { Animal* animals[] = { new Dog(), new Cat() }; for (auto* a : animals) { a->speak(); // Calls correct version: Woof! then Meow! // Decided at RUNTIME via vtable } delete animals[0]; delete animals[1]; }

Java equivalent (all methods are virtual by default in Java):

abstract class Animal { abstract void speak(); } class Dog extends Animal { void speak() { System.out.println("Woof!"); } } class Cat extends Animal { void speak() { System.out.println("Meow!"); } } Animal[] animals = { new Dog(), new Cat() }; for (Animal a : animals) { a.speak(); // Woof! then Meow! — runtime dispatch }

Likely follow-up:

"What is the vtable overhead?" Each object gets 1 extra hidden pointer (vptr, ~8 bytes). Each class has 1 vtable (shared — not per object). Each virtual call has ~2 extra memory accesses. Usually negligible unless in an extremely tight loop.

Common mistake: Calling virtual functions inside a constructor or destructor. During base class construction, the object IS a Base — the derived vtable is not yet set up. The BASE version always runs, not the derived override.

class Base { public: Base() { init(); } // Calls Base::init() ALWAYS, even for Derived objects! virtual void init() { cout << "Base init"; } }; class Derived : public Base { void init() override { cout << "Derived init"; } }; Derived d; // Prints "Base init", NOT "Derived init"

4. Pure Virtual Functions and Abstract Classes

Definition:

A pure virtual function (= 0) has no implementation in the base class and MUST be overridden by any concrete derived class. A class with at least one pure virtual function is abstract — it cannot be instantiated.

Key Point:

  • Abstract classes define the CONTRACT — what derived classes must implement
  • You CAN have pointers/references to abstract classes (enables polymorphism)
  • Derived class must implement ALL pure virtual functions, or it is also abstract
class Shape { public: virtual double area() const = 0; // Must override virtual double perimeter() const = 0; // Must override virtual void draw() const = 0; // Must override // Non-pure virtual — has default, can be overridden virtual string color() const { return "white"; } virtual ~Shape() = default; }; // Shape s; // ERROR! Cannot instantiate abstract class class Circle : public Shape { double r; public: Circle(double r) : r(r) {} double area() const override { return 3.14159 * r * r; } double perimeter() const override { return 2 * 3.14159 * r; } void draw() const override { cout << "Drawing circle r=" << r; } }; // Polymorphism in action vector<Shape*> shapes = { new Circle(5), new Circle(3) }; for (auto* s : shapes) { cout << s->area() << endl; // Correct area for each shape }

Likely follow-up:

"What is an interface in C++?" C++ has no interface keyword. An interface is an abstract class with ONLY pure virtual functions and no data members. Java has explicit interface keyword. From Java 8, interfaces can have default methods (like C++ non-pure virtual functions).

Common mistake: Forgetting to override ONE pure virtual function in a derived class — the derived class becomes abstract too and cannot be instantiated. Compiler error: "cannot instantiate abstract class."


5. Method Overriding vs Hiding — Know the Difference

Definition:

Overriding: replacing a virtual function in a derived class — dynamic dispatch uses the override. Hiding: writing a non-virtual function in derived with same name as base — base pointer still calls BASE version.

class Base { public: virtual void foo(int x) { cout << "Base::foo(int)"; } void bar() { cout << "Base::bar()"; } // NOT virtual }; class Derived : public Base { public: void foo(int x) override { cout << "Derived::foo(int)"; } // OVERRIDING void bar() { cout << "Derived::bar()"; } // HIDING }; int main() { Derived d; Base* bp = &d; bp->foo(1); // "Derived::foo(int)" — virtual dispatch (override works) bp->bar(); // "Base::bar()" — static dispatch (hiding, base runs!) d.bar(); // "Derived::bar()" — called directly on Derived, works }

Rule: Always use override keyword when you intend to override — it catches bugs at compile time. Missing const, wrong parameter type, or typo in name will cause a compile error instead of silent hiding.


6. Compile-Time vs Runtime — Quick Comparison

FeatureCompile-TimeRuntime
Also calledStatic / Early bindingDynamic / Late binding
Resolved whenCompile timeRuntime
MechanismOverloading, templatesvirtual functions (vtable)
SpeedFaster — no indirectionSmall overhead (vtable lookup)
Keyword neededNonevirtual
Java defaultSame conceptALL methods are virtual by default
C++ exampleadd(int) vs add(double)shape->area() calls right version

7. Rapid-Fire Self-Test — Polymorphism

(Answer in one breath)

  1. What are the two types of polymorphism?
  2. What makes Round Robin scheduling an analogy for runtime polymorphism? (Hint: the scheduler doesn't know which process it runs next)
  3. What does the override keyword catch?
  4. Difference between hiding and overriding?
  5. What is a vtable and when is it created?
  6. Can you put different shapes in a vector<Shape*>? Why?
  7. If base has void bar() (no virtual) and derived has void bar(), what happens when called through base pointer?
  8. What is the signature of a post-increment operator?
  9. Why must operator<< be a friend (or non-member)?
  10. In Java, do you need virtual for runtime polymorphism?

Answers

  1. Compile-time (function/operator overloading) and runtime (virtual functions)
  2. The dispatcher does not know which function to call until runtime — based on the actual object type, just like a scheduler not knowing task length
  3. That the function actually matches a virtual function in the base — catches typos, wrong param types, missing const
  4. Hiding: non-virtual, base pointer calls base version. Overriding: virtual, base pointer calls derived version via vtable
  5. Compiler-generated array of function pointers per class. Created at compile time, linked into the binary
  6. Yes — because all elements share a common base type (Shape*). The virtual dispatch handles calling the right area()
  7. The BASE version is called — it is static binding, not virtual dispatch
  8. ClassName operator++(int) — takes a dummy int parameter to distinguish from pre-increment
  9. The left operand of << is ostream, not your class — member function requires your class as left operand
  10. No — all non-static, non-final methods in Java are virtual by default