Polymorphism
Instead of creating different function names for every type of object, polymorphism allows the same interface to perform different tasks. In this lesson, you will learn compile-time polymorphism through function overloading and run-time polymorphism through virtual functions and overriding.
Introduction
In the previous lesson, you learned how inheritance allows a derived class to reuse and extend the features of a base class.
Now imagine that you are building a graphics application containing Circle, Rectangle, and Triangle objects. Every shape needs to perform the same general operation: draw itself.
drawCircle()
drawRectangle()
drawTriangle()Creating a different function name for every object makes code harder to generalize. Instead, we would like every shape to provide the same draw() operation while allowing each shape to perform that operation differently.
Circle → draw() → Draw a Circle
Rectangle → draw() → Draw a Rectangle
Triangle → draw() → Draw a TriangleSeparate Interfaces
- drawCircle() for circles.
- drawRectangle() for rectangles.
- drawTriangle() for triangles.
- Calling code must know every specific type.
Common Interface
- Every shape provides draw().
- Each object performs its own behavior.
- Calling code can work with a common type.
- New shape types are easier to add.
Polymorphism allows the same operation or interface to represent different behaviors.
What is Polymorphism?
The word polymorphism comes from the Greek words poly, meaning many, and morph, meaning forms. Therefore, polymorphism literally means many forms.
In programming, polymorphism allows the same function name, operation, or interface to behave differently depending on the arguments, the context, or the actual object involved.
Same Interface
│
┌─────────┼─────────┐
▼ ▼ ▼
Form 1 Form 2 Form 3
Circle Rectangle Triangle
│ │ │
▼ ▼ ▼
draw() draw() draw()| Concept | Meaning |
|---|---|
| Poly | Many |
| Morph | Forms |
| Polymorphism | One interface with multiple forms of behavior |
| Compile-Time Polymorphism | Behavior selected during compilation |
| Run-Time Polymorphism | Behavior selected while the program runs |
Same operation: makeSound()
Dog → makeSound() → Bark
Cat → makeSound() → Meow
Cow → makeSound() → MooPolymorphism means using a common operation while allowing that operation to take different forms.
Why Do We Need Polymorphism?
Applications often contain related operations that differ only by data type or object type. Without polymorphism, programmers may create many separate function names or large conditional structures.
Without Polymorphism
- Different function names are often created.
- Calling code depends on specific object types.
- Conditional logic can grow.
- Adding new types may require modifying existing logic.
- Applications become harder to extend.
With Polymorphism
- Related operations can share an interface.
- Behavior can vary by arguments or object type.
- Calling code can become more general.
- New implementations can be easier to add.
- Application design becomes more flexible.
void drawCircle()
{
// Draw circle
}
void drawRectangle()
{
// Draw rectangle
}
void drawTriangle()
{
// Draw triangle
}Shape Interface
│
└── draw()
│
┌─────┼─────┐
▼ ▼ ▼
Circle Rect Triangle
draw() draw() draw()Common Interface
Related operations can use the same function name or interface.
Flexibility
Behavior can change according to arguments or actual object type.
Extensibility
New implementations can often be added with less change to existing code.
Maintainability
Type-specific behavior can remain inside the appropriate class.
Less Conditional Logic
Virtual dispatch can replace some large type-checking structures.
Clear Design
Common concepts can expose consistent operations.
Real-World Analogy
Imagine a person named Rahul. The same person can perform different roles depending on the context.
👤 Rahul
│
┌───────────┼───────────┐
▼ ▼ ▼
🏠 Home 🏢 Office 🎓 College
│ │ │
▼ ▼ ▼
Son Employee StudentThe person remains the same, but the role and behavior change according to the situation. This provides a simple analogy for one interface appearing in multiple forms.
| Real-World Analogy | Programming Concept |
|---|---|
| Rahul | Common entity |
| Home, office, college | Different contexts |
| Son, employee, student | Different forms or roles |
| Context-dependent behavior | Polymorphic behavior |
C++ polymorphism is based on language rules such as overload resolution and virtual dispatch. The real-world analogy only helps explain the general idea of multiple forms.
Types of Polymorphism
C++ polymorphism is commonly divided into two broad categories: compile-time polymorphism and run-time polymorphism.
1️⃣ Compile-Time Polymorphism
The compiler determines which operation to use before the program runs.
2️⃣ Run-Time Polymorphism
The program determines the final overridden function according to the actual object during execution.
Polymorphism
│
├── Compile-Time Polymorphism
│ ├── Function Overloading
│ └── Operator Overloading
│
└── Run-Time Polymorphism
├── Inheritance
├── Function Overriding
└── Virtual Functions| Type | Also Called | Decision Time | Common Mechanism |
|---|---|---|---|
| Compile-Time | Static Polymorphism | During compilation | Function and operator overloading |
| Run-Time | Dynamic Polymorphism | During execution | Virtual functions and overriding |
Compile-Time
- Resolved by the compiler.
- Commonly uses overloading.
- Does not require inheritance.
- Selection depends on available signatures.
Run-Time
- Resolved through dynamic dispatch.
- Uses virtual functions.
- Requires an inheritance relationship.
- Selection depends on the actual object type.
Compile-time and run-time polymorphism both provide multiple forms of behavior, but they are implemented and selected differently.
1. Compile-Time: Function Overloading
Function overloading allows multiple functions to use the same name when their parameter lists are different.
When an overloaded function is called, the compiler examines the supplied arguments and selects the best matching function.
void show(int value);
void show(double value);
void show(char value);| Function | Parameter Type | Called With |
|---|---|---|
| show(int) | int | 10 |
| show(double) | double | 25.5 |
| show(char) | char | 'A' |
#include <iostream>
class Display
{
public:
void show(int number)
{
std::cout << "Integer : "
<< number << std::endl;
}
void show(double number)
{
std::cout << "Decimal : "
<< number << std::endl;
}
void show(char letter)
{
std::cout << "Character : "
<< letter << std::endl;
}
};
int main()
{
Display d;
d.show(10);
d.show(25.5);
d.show('A');
return 0;
}How the Program Works
- The Display class contains three functions named show().
- Each show() function has a different parameter type.
- d.show(10) provides an int argument.
- The compiler selects show(int).
- d.show(25.5) provides a double argument.
- The compiler selects show(double).
- d.show('A') provides a char argument.
- The compiler selects show(char).
d.show(10)
│
└── int argument
│
▼
show(int)
d.show(25.5)
│
└── double argument
│
▼
show(double)
d.show('A')
│
└── char argument
│
▼
show(char)Integer : 10
Decimal : 25.5
Character : AOverloaded functions must differ in their parameter lists. Changing only the return type is not enough to create a valid overload.
// ❌ Invalid
int calculate(int value);
double calculate(int value);Some combinations of overloads and arguments can produce ambiguity when the compiler cannot determine a unique best match.
2. Run-Time: Virtual Functions & Overriding
Run-time polymorphism allows a call made through a base-class pointer or reference to execute an overridden function belonging to the actual derived object.
This behavior is enabled by declaring the base-class function virtual.
class Base
{
public:
virtual void perform()
{
// Base implementation
}
};
class Derived : public Base
{
public:
void perform() override
{
// Derived implementation
}
};| Keyword | Purpose |
|---|---|
| virtual | Enables dynamic dispatch for the function |
| override | Confirms that a derived function overrides a virtual base function |
| Base pointer/reference | Provides access through the common base interface |
| Derived object | Determines the final overridden behavior at run time |
#include <iostream>
class Animal
{
public:
virtual void sound()
{
std::cout << "Animal Sound"
<< std::endl;
}
};
class Dog : public Animal
{
public:
void sound() override
{
std::cout << "Dog Barks"
<< std::endl;
}
};
int main()
{
Animal* animalPtr;
Dog dog;
animalPtr = &dog;
animalPtr->sound();
return 0;
}How the Program Works
- Animal defines the virtual sound() function.
- Dog publicly inherits from Animal.
- Dog overrides sound().
- animalPtr is a pointer of type Animal*.
- dog is an object of type Dog.
- animalPtr is assigned the address of dog.
- animalPtr->sound() is called through the base pointer.
- Because sound() is virtual, the actual Dog object determines the function.
- Dog::sound() executes.
Animal* animalPtr
│
│ points to
▼
Dog object
│
│ animalPtr->sound()
▼
Virtual dispatch
│
▼
Dog::sound()Dog BarksWithout virtual
- Call is selected using the static expression type.
- A base-pointer call selects the base function.
- Derived overriding behavior is not dynamically dispatched.
With virtual
- The actual object participates in function selection.
- A base pointer can invoke derived behavior.
- Dynamic dispatch enables run-time polymorphism.
class Animal
{
public:
void sound()
{
std::cout << "Animal Sound";
}
};class Animal
{
public:
virtual void sound()
{
std::cout << "Animal Sound";
}
};Use the override specifier on derived overriding functions. It allows the compiler to detect signature mistakes.
class Animal
{
public:
virtual void sound()
{
}
};
class Dog : public Animal
{
public:
void sound() override
{
}
};If objects may be deleted through a base-class pointer, the base class should normally have a virtual destructor.
class Animal
{
public:
virtual ~Animal() = default;
virtual void sound()
{
}
};Important Comparisons
Overloading and overriding both reuse function names, but they solve different problems and follow different rules.
Function Overloading vs Function Overriding
| Feature | Function Overloading | Function Overriding |
|---|---|---|
| Purpose | Provide related operations for different arguments | Replace virtual base behavior in a derived class |
| Typical Scope | Same scope or class | Base and derived classes |
| Function Name | Same | Same |
| Parameter List | Different | Matching |
| Inheritance Required | No | Yes |
| virtual Required | No | Required for dynamic dispatch |
| Decision Time | Compile time | Run time for virtual calls |
| Keyword | No special keyword | override recommended |
class Display
{
public:
void show(int value);
void show(double value);
};class Animal
{
public:
virtual void sound();
};
class Dog : public Animal
{
public:
void sound() override;
};Compile-Time vs Run-Time Polymorphism
| Feature | Compile-Time Polymorphism | Run-Time Polymorphism |
|---|---|---|
| Also Called | Static polymorphism | Dynamic polymorphism |
| Selection | During compilation | During execution |
| Common Mechanism | Function/operator overloading | Virtual functions |
| Inheritance Required | No | Yes |
| Object Type Used | Static argument and expression information | Actual dynamic object type |
| Flexibility | Fixed by compile-time resolution | Behavior can vary through a common base interface |
| Dispatch Overhead | Normally no virtual-dispatch overhead | May involve virtual-dispatch overhead |
Static Selection
- Compiler selects the operation.
- Uses available overload signatures.
- Based on argument types.
- Example: show(int) vs show(double).
Dynamic Selection
- Virtual dispatch selects the final override.
- Uses the actual object type.
- Works through a base pointer or reference.
- Example: Animal interface calling Dog behavior.
Overloading means multiple functions with different parameter lists. Overriding means a derived class provides a replacement implementation for a matching virtual base-class function.
Real-World Applications
Polymorphism appears whenever software needs a common operation with type-specific implementations.
Banking
calculateInterest() can behave differently for different account types.
Payment Systems
pay() can be implemented differently for cards, UPI, wallets, and net banking.
Game Development
attack() can behave differently for warriors, mages, enemies, and bosses.
Graphics Software
draw() can render circles, rectangles, triangles, and other shapes.
Notification Systems
send() can deliver email, SMS, and push notifications differently.
File Processing
open() or parse() can behave differently for different file formats.
Payment
│
└── pay()
│
┌────┼─────────┐
▼ ▼ ▼
Card UPI Net Banking
pay() pay() pay()Character
│
└── attack()
│
┌────┼────┐
▼ ▼ ▼
Warrior Mage Archer
attack attack attackThe main benefit is that calling code can work with the common operation while each implementation controls its own behavior.
Advantages of Polymorphism
Interface Reuse
The same operation can represent related behaviors.
Flexibility
Code can work with general interfaces instead of every concrete type.
Extensibility
New derived implementations can often be added with fewer changes to calling code.
Maintainability
Type-specific behavior remains inside the appropriate implementation.
Reduced Conditionals
Dynamic dispatch can replace some explicit type checks and switch statements.
Better Architecture
Common abstractions can separate what an operation does from how each type performs it.
- Allows related operations to share a common interface.
- Supports function reuse with different argument types.
- Enables derived objects to provide specialized behavior.
- Reduces dependence on concrete classes.
- Can reduce large type-based conditional structures.
- Makes many systems easier to extend.
- Supports modular object-oriented design.
- Works as a foundation for interface-driven architecture.
Unnecessary virtual hierarchies can make code harder to understand. Use polymorphism when multiple implementations genuinely need to share an operation or abstraction.
Common Beginner Mistakes
Overloading uses different parameter lists. Overriding replaces matching virtual behavior in a derived class.
Without a virtual base function, calls through a base pointer or reference do not use virtual dispatch.
A different parameter list does not override the intended base function.
Without override, a signature mistake may silently create a different function instead of an override.
Function overloading can work entirely within one class or scope.
A polymorphic base intended for deletion through a base pointer should normally have a virtual destructor.
// Overloading
void show(int value);
void show(double value);
// Overriding
class Base
{
public:
virtual void show();
};
class Derived : public Base
{
public:
void show() override;
};class Animal
{
public:
// ❌ No dynamic dispatch
void sound()
{
std::cout << "Animal";
}
};class Animal
{
public:
virtual void sound()
{
std::cout << "Animal";
}
};class Animal
{
public:
virtual void sound()
{
}
};
class Dog : public Animal
{
public:
// ❌ Different parameter list
void sound(int volume)
{
}
};class Dog : public Animal
{
public:
void sound() override
{
}
};Use override whenever you intend to override a virtual function. The compiler will report mismatched signatures.
Best Practices
- Use function overloading for closely related operations with meaningfully different parameter lists.
- Do not overload unrelated operations only because they can share a name.
- Use virtual functions when behavior must depend on the actual object type.
- Use override for every intended derived override.
- Prefer base references or smart pointers when working with polymorphic objects.
- Give polymorphic base classes appropriate virtual destructors.
- Keep the base-class interface focused and meaningful.
- Avoid unnecessary type checks when virtual dispatch can express the behavior directly.
- Do not create inheritance hierarchies only to use polymorphism.
- Keep overridden functions behaviorally consistent with the base abstraction.
- Prefer clear ownership when storing polymorphic objects.
- Use abstract interfaces when a base class should define behavior without providing a meaningful default implementation.
Type Checking
- Check whether object is Circle.
- Else check Rectangle.
- Else check Triangle.
- Modify logic whenever a new type is added.
Virtual Interface
- Call shape.draw().
- Each shape controls its implementation.
- Calling code depends on the abstraction.
- New implementations can fit the common interface.
class Shape
{
public:
virtual ~Shape() = default;
virtual void draw() = 0;
};
class Circle : public Shape
{
public:
void draw() override
{
std::cout << "Drawing Circle";
}
};When appropriate, let calling code depend on the base abstraction while derived classes provide the specialized behavior.
Frequently Asked Questions
What is polymorphism?
Polymorphism is the ability of a common operation or interface to represent different forms of behavior.
What does polymorphism mean literally?
It means many forms.
What are the two main types of polymorphism in C++?
Compile-time polymorphism and run-time polymorphism.
What is compile-time polymorphism?
It is polymorphism where the compiler selects the operation before execution, commonly through function or operator overloading.
What is run-time polymorphism?
It is polymorphism where virtual dispatch selects an overridden function according to the actual object during execution.
What is function overloading?
Function overloading means defining multiple functions with the same name but different parameter lists.
Can functions be overloaded by changing only the return type?
No. The parameter lists must provide a valid distinction.
What is function overriding?
Function overriding occurs when a derived class provides its own implementation of a matching virtual base-class function.
What does the virtual keyword do?
It enables dynamic dispatch for calls made through suitable base-class pointers or references.
What does override do?
It tells the compiler that a derived function is intended to override a virtual base-class function and enables signature checking.
Does function overloading require inheritance?
No. Overloading can occur without inheritance.
Does run-time polymorphism require inheritance?
Traditional virtual-function run-time polymorphism requires a base and derived class relationship.
Why should a polymorphic base class have a virtual destructor?
A virtual destructor ensures correct destruction when a derived object is deleted through a base-class pointer.
What determines which overloaded function is called?
The compiler performs overload resolution using the available function signatures and supplied arguments.
What determines which overridden virtual function is called?
The actual dynamic type of the object determines the final override during virtual dispatch.
Key Takeaways
- Polymorphism means many forms.
- A common operation can represent different behaviors.
- C++ commonly divides polymorphism into compile-time and run-time forms.
- Compile-time polymorphism is also called static polymorphism.
- Run-time polymorphism is also called dynamic polymorphism.
- Function overloading is a common form of compile-time polymorphism.
- Overloaded functions use the same name with different parameter lists.
- Changing only the return type does not create a valid overload.
- Virtual functions enable dynamic dispatch.
- Function overriding occurs between base and derived classes.
- An overriding function should match the intended virtual base signature.
- The override specifier helps the compiler detect mistakes.
- A base pointer or reference can invoke derived behavior through a virtual function.
- The actual object type determines the final virtual override.
- Overloading does not require inheritance.
- Run-time polymorphism through virtual functions requires an inheritance relationship.
- Polymorphic base classes should be designed carefully.
- A base class used for polymorphic deletion should normally have a virtual destructor.
- Polymorphism can reduce explicit type checking and improve extensibility.
- Polymorphism is a fundamental object-oriented programming concept.
Summary
Polymorphism allows a common function name, operation, or interface to represent multiple forms of behavior.
Compile-time polymorphism is resolved by the compiler. Function overloading is a common example, where several functions share a name but use different parameter lists.
Run-time polymorphism uses inheritance, virtual functions, and overriding. A call through a base-class pointer or reference can execute the appropriate derived-class implementation according to the actual object.
The virtual keyword enables dynamic dispatch, while override allows the compiler to verify that a derived function correctly overrides a base-class virtual function.
By understanding overloading, overriding, virtual functions, and dynamic dispatch, you can design programs that use common interfaces while allowing different objects to provide specialized behavior.
Polymorphism builds directly on inheritance and prepares you for abstraction, where classes expose essential behavior while hiding unnecessary implementation details.