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:

⚡ Compile-Time Polymorphism

Resolved at compile time

Static polymorphism / Early binding

⚡ Runtime Polymorphism

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:

When

Resolution at compile time

Based On

Declared type of variable

Performance

No runtime overhead

Mechanisms

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:

1. First

Check derived class

2. Then

Search up inheritance hierarchy

3. Finally

Use first match found

Listing 4 Method Resolution Example
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.

Listing 5 Method Redefinition Example
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:

1. Virtual Method

A virtual method in base class

2. Base Handle

Call through base reference/pointer

3. Dynamic Dispatch

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:

⏰ Runtime Resolution

Method call resolved at runtime, not compile time

🎯 Actual Type

Actual object type determines which method runs

📊 VTable

Compiler creates virtual method table

Listing 6 Virtual Method Declaration
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);
};
Listing 7 Dynamic Dispatch in Action
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 not auto rt = ... which is equivalent to std::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)
Listing 8 Overloads for Each Type
// 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)
Listing 9 One Function for All Types
// 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.

Listing 10 Base Type from Start
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

Listing 11 Concrete Type Initially
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

✅ This WORKS

Function parameter accepts conversion:

auto rt = std::make_unique<RoboTaxi>(...);
run_shift(std::move(rt));  // ✓ Converts at call
❌ This FAILS

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 auto

Pass to function with base parameter

Store in ANY container of base pointers

Use push_back/emplace_back/insert with variable

Direct insertion: vec.push_back(make_unique<...>())

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

Listing 12 Conversions Work at Call Boundary
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
Listing 13 Template Types Don’t Convert
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

Listing 14 Best Practice
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
Listing 15 Clear Intent
std::unique_ptr<Vehicle> rt = std::make_unique<RoboTaxi>(...);
rt->drive();  // Polymorphic call
fleet.push_back(std::move(rt));  // ✓ Types match exactly
Listing 16 Avoid - Verbose and Error-Prone
auto rt = std::make_unique<RoboTaxi>(...);
fleet.push_back(std::unique_ptr<Vehicle>(std::move(rt)));  // ✓ But ugly

Decision Framework

Container Storage?

→ Use explicit base type OR direct insertion

Polymorphic Calls at Use?

→ Use explicit base type

Derived-Specific Methods First?

→ Use auto, then pass to functions

Only Function Calls?

→ 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:

Catches Typos

If signatures don’t match, compilation fails

Documents Intent

Makes it clear this overrides a base method

Prevents Hiding

Detects when you think you’re overriding but aren’t

Listing 17 Using override for Safety
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

Listing 18 Polymorphic Collection Example
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

1.Inheritance ✓

Derived classes inherit from common base (Vehicle)

2. Base Handle ✓

Use Vehicle&, Vehicle*, unique_ptr<Vehicle>

3. Virtual Method ✓

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 virtual in base classes for methods that derived classes will override

  • Always use override in derived classes for safety

  • Prefer explicit base type when building polymorphic collections

  • Use auto when you need concrete type initially, then pass to functions

  • Remember: unique_ptr<Derived>unique_ptr<Base> for containers

  • Direct insertion works best for polymorphic collections

Compile-Time

Static resolution

Runtime

Dynamic dispatch

Virtual

Enable polymorphism

Override

Safety feature