Polymorphism¶
Definition: Polymorphism (Greek: poly = many, morph = form) is a core OOP principle. Polymorphism allows objects of different classes to be treated uniformly through a common interface.
C++ supports two types of polymorphism:
Resolved at compile time
Static polymorphism / Early binding
Resolved at runtime
Dynamic polymorphism / Late binding
Compile-Time Polymorphism¶
Definition: Compile-time polymorphism (also called static polymorphism or early binding) is resolved during compilation. The compiler determines which function to call based on the static type at compile time.
Key Characteristics:
Resolution at compile time
Declared type of variable
No runtime overhead
Overloading & redefinition
Note
The function resolution happens at compile time based on the declared type of the variable, not the actual object it contains.
Method Order Check¶
When a method is called on a derived class object:
Check derived class
Search up inheritance hierarchy
Use first match found
class Base {
public:
void test() {
std::cout << "Base::test()\n";
}
};
class Derived : public Base {
// No test() method defined
};
int main() {
Derived derived;
derived.test(); // Calls Base::test()
}
Method Redefinition¶
Definition: Method redefinition allows a derived class to provide its own implementation of a base class method when the base class version is too general or needs specialization.
class Base {
public:
void test() {
std::cout << "Base::test()\n";
}
};
class Derived : public Base {
public:
void test() { // Redefines Base::test()
std::cout << "Derived::test()\n";
}
};
int main() {
Derived derived;
derived.test(); // Calls Derived::test()
}
Note
Method redefinition is compile-time polymorphism because the method selection is based on the static type known at compile time.
Runtime Polymorphism¶
Definition: Runtime polymorphism (dynamic polymorphism or late binding) decides which virtual method implementation runs based on the dynamic type (actual object type) at runtime, not the static type of the pointer/reference.
Key Requirements:
A virtual method in base class
Call through base reference/pointer
Method depends on actual object type
Important
Runtime polymorphism in C++ requires a virtual method in a base class and a call through a base reference or base pointer to a derived object. The actual method called depends on the real object type at runtime.
The virtual Keyword¶
Definition: The virtual keyword tells the compiler to use dynamic dispatch for a method.
What It Does:
Method call resolved at runtime, not compile time
Actual object type determines which method runs
Compiler creates virtual method table
class Vehicle {
public:
// virtual -> can be overridden in derived classes
virtual void drive();
// NOT virtual -> cannot be overridden (static binding)
void update_location(const Location& location);
};
int main() {
using transportation::Vehicle;
using transportation::RoboTaxi;
// Actual object: RoboTaxi
// Pointer type: Vehicle
std::unique_ptr<Vehicle> rt =
std::make_unique<RoboTaxi>("ROBOTAXI-001", 4);
rt->drive(); // Calls RoboTaxi::drive() - dynamic dispatch
rt->update_location(loc); // Calls Vehicle::update_location() - static binding
}
Warning
Without
virtual, C++ uses static binding based on the pointer type, not the actual object!Note that we wrote:
std::unique_ptr<Vehicle> rt = ...and notauto rt = ...which is equivalent tostd::unique_ptr<RoboTaxi> rt. Remember that we want a base class pointer (or reference) to a derived object for polymorphism to work.
The Problem Without Polymorphism¶
Challenge: You must drive different vehicle types uniformly (RoboTaxi, Taxi, …), yet each type performs the task differently.
❌ Non-Polymorphic Solution (Not Scalable)
// Overloads choose at compile time based on the static type
void run_shift(transportation::RoboTaxi& v) { v.drive(); }
void run_shift(transportation::Taxi& v) { v.drive(); }
int main() {
transportation::RoboTaxi rt{"ROBOTAXI-001", 4};
transportation::Taxi tx{"TAXI-001", 4};
run_shift(rt); // Calls RoboTaxi version
run_shift(tx); // Calls Taxi version
}
Warning
Overload resolution is a compile-time choice. Each new derived type requires another overload. Bodies tend to duplicate (violates DRY principle). Does not support heterogeneous collections.
✅ Polymorphic Solution (Scalable)
// Base interface with virtual method
class Vehicle {
public:
virtual ~Vehicle() = default; // Essential for polymorphic bases
virtual void drive(); // Virtual (can be overridden)
};
// One function works for ALL current and future derived vehicle types
void run_shift(transportation::Vehicle& v) {
v.drive(); // Runtime dispatch - calls the actual object's drive()
}
// Pointer variant (e.g., ownership with unique_ptr)
void run_shift(std::unique_ptr<transportation::Vehicle> v) {
v->drive(); // Runtime dispatch
}
int main() {
auto rt = std::make_unique<transportation::RoboTaxi>("ROBOTAXI-001", 4);
auto tx = std::make_unique<transportation::Taxi>("TAXI-001", 4);
run_shift(*rt); // Calls RoboTaxi::drive()
run_shift(std::move(tx)); // Calls Taxi::drive()
}
Important
Only one run_shift() is needed. The call site uses a base reference or pointer. The target method is determined at runtime based on the actual object type (the most-derived override).
When to Use auto vs. Explicit Base Type¶
Question: When should you use auto versus an explicit base type?
Answer: The choice depends on where polymorphism needs to happen.
std::unique_ptr<Vehicle> rt = std::make_unique<RoboTaxi>(...);
// Polymorphic calls directly
rt->drive();
Use when:
✓ Multiple polymorphic calls needed locally
✓ Polymorphism happens at point of creation
✓ Building polymorphic collections
auto rt = std::make_unique<RoboTaxi>(...);
// Direct calls to RoboTaxi
rt->drive();
// Pass to polymorphic function
run_shift(std::move(rt));
Use when:
✓ Concrete type needed initially
✓ Polymorphism delegated to function calls
✓ Need derived-specific methods before passing to functions
The Container Problem with auto¶
Function parameter accepts conversion:
auto rt = std::make_unique<RoboTaxi>(...);
run_shift(std::move(rt)); // ✓ Converts at call
Container requires exact type match:
auto rt = std::make_unique<RoboTaxi>(...);
std::vector<std::unique_ptr<Vehicle>> fleet;
fleet.push_back(std::move(rt)); // ✗ Type mismatch!
Warning
Why? Function parameters allow implicit conversions at the call site. Container storage requires exact type matches: unique_ptr<RoboTaxi> ≠ unique_ptr<Vehicle>. This applies to ALL containers (vector, deque, list, set, map, etc.).
Complete Comparison Table¶
Operation |
Explicit Base Type |
Using |
|---|---|---|
Pass to function with base parameter |
✓ |
✓ |
Store in ANY container of base pointers |
✓ |
✗ |
Use |
✓ |
✗ |
Direct insertion: |
✓ |
N/A* |
Call derived-specific methods |
✗ |
✓ |
Polymorphic calls at point of use |
✓ |
✗ |
* No auto variable involved — temporary converts at insertion point
Why This Happens: Type System Rules¶
void run_shift(std::unique_ptr<Vehicle> v); // Accepts base type
auto rt = std::make_unique<RoboTaxi>(...); // unique_ptr<RoboTaxi>
run_shift(std::move(rt)); // ✓ Compiler converts RoboTaxi* → Vehicle*
// Conversion happens at call boundary
std::vector<std::unique_ptr<Vehicle>> fleet; // Template parameter is FIXED
auto rt = std::make_unique<RoboTaxi>(...); // unique_ptr<RoboTaxi>
fleet.push_back(std::move(rt)); // ✗ Template types don't convert!
// unique_ptr<RoboTaxi> ≠ unique_ptr<Vehicle>
Warning
Key insight: unique_ptr<Derived> is NOT a subtype of unique_ptr<Base>, even though Derived* converts to Base*. Template instantiations don’t inherit type relationships!
Solutions for Polymorphic Collections¶
std::vector<std::unique_ptr<Vehicle>> fleet;
fleet.push_back(std::make_unique<RoboTaxi>(...)); // ✓ Implicit conversion
fleet.push_back(std::make_unique<Taxi>(...)); // ✓ Implicit conversion
std::unique_ptr<Vehicle> rt = std::make_unique<RoboTaxi>(...);
rt->drive(); // Polymorphic call
fleet.push_back(std::move(rt)); // ✓ Types match exactly
auto rt = std::make_unique<RoboTaxi>(...);
fleet.push_back(std::unique_ptr<Vehicle>(std::move(rt))); // ✓ But ugly
Decision Framework¶
→ Use explicit base type OR direct insertion
→ Use explicit base type
→ Use auto, then pass to functions
→ Either works; auto is more flexible
Important
Rule of thumb: If you’re building polymorphic collections, use explicit base type or direct insertion. If you’re only calling functions, auto is fine.
The override Keyword¶
Definition: The override keyword is a safety feature that tells the compiler “I intend this method to override a base class virtual method”.
Benefits:
If signatures don’t match, compilation fails
Makes it clear this overrides a base method
Detects when you think you’re overriding but aren’t
class Vehicle {
public:
virtual void drive();
};
class RoboTaxi : public Vehicle {
public:
void drive() override; // OK - matches base signature
// void drive(int) override; // ERROR - no matching virtual in base
// void driev() override; // ERROR - typo caught!
};
Important
Always use override when overriding virtual methods. It’s free safety!
Avoid Slicing & Embrace Polymorphic Collections¶
std::vector<std::unique_ptr<transportation::Vehicle>> fleet;
fleet.emplace_back(std::make_unique<transportation::RoboTaxi>("ROBOTAXI-001", 4));
fleet.emplace_back(std::make_unique<transportation::Taxi>("TAXI-001", 4));
for (auto& v : fleet) {
v->drive(); // Runtime dispatch for each element's actual type
}
Warning
Do not store derived objects by value in a std::vector<Vehicle>. This causes object slicing where derived class data is lost and polymorphism doesn’t work. Use owning pointers (unique_ptr, shared_ptr), or non-owning pointers/references with external lifetime management.
Requirements for Runtime Polymorphism¶
Derived classes inherit from common base (Vehicle)
Use Vehicle&, Vehicle*, unique_ptr<Vehicle>
Mark interface virtual; override in derived classes
Note
The call target depends on the dynamic type of the object (what it actually is at runtime), not the static type of the handle (how the pointer/reference was declared). This is late binding.
Core Concepts:
Compile-time polymorphism: Function/operator overloading, method redefinition
Runtime polymorphism: Virtual methods with dynamic dispatch
Use
virtualin base classes for methods that derived classes will overrideAlways use
overridein derived classes for safetyPrefer explicit base type when building polymorphic collections
Use
autowhen you need concrete type initially, then pass to functionsRemember:
unique_ptr<Derived>≠unique_ptr<Base>for containersDirect insertion works best for polymorphic collections
Static resolution
Dynamic dispatch
Enable polymorphism
Safety feature