Historical Context
To appreciate polymorphism, we need to understand how programs were written before object-oriented languages became popular.
In procedural programming, behavior was usually selected using conditional statements.
Imagine writing a graphics application in C.
struct Shape
{
int type;
};Then:
void draw(Shape* shape)
{
switch (shape->type)
{
case CIRCLE:
drawCircle(shape);
break;
case RECTANGLE:
drawRectangle(shape);
break;
case TRIANGLE:
drawTriangle(shape);
break;
}
}This worked initially.
But imagine that the application grows.
Now it supports:
- Circle
- Rectangle
- Triangle
- Ellipse
- Polygon
- Star
- Hexagon
- Bezier Curve
Every time a new shape is introduced:
- Add a new type.
- Modify the
switch - Recompile
- Retest everything.
The central function grows larger and becomes responsible for knowing every possible shape.
This design has several drawbacks:
- It violates the Open/Closed Principle
- It tightly couples behavior to a central dispatcher.
- Every time a new feature requires modifying existing code.
- The code becomes increasingly difficult to maintain.
Object-oriented programming introduced a different idea.
Instead of asking:
βWhat type are you?β
and then decide what to do,
we simply tell the object:
βDraw yourself.β
The object knows how to perform the operation appropriate to its own type.
This shift from external decision-making to object responsibility is one of the defining ideas of object-oriented design.
What Is Polymorphism?
The word polymorphism comes from two Greek words:
- Poly = many
- Morph = forms
Literally:
Many forms
In software engineering, polymorphism means:
One interface, many implementations.
Consider a remote control.
Press Power Button
β
TV turns on
β
Air Conditioner turns on
β
Projector turns onThe user performs the same action:
Power()Different devices perform different implementations.
The interface is the same.
The behavior varies.
That is polymorphism.
Formal Definition
Polymorphism is the ability to use a common interface while allowing different object types to provide their own behavior.
What Does βMany Formsβ Really Mean?
Recall the literal meaning of polymorphism:
One interface, many forms.
It doesn't say:
One object, many forms.
It says:
One interface can have many implementations.
The way those implementations are selected differs depending on the type of polymorphism.
Sometimes the compiler decides.
Sometimes the runtime decides.
This leads to the two major categories.
Polymorphism
/ \
Compile-Time Run-Time
(Static) (Dynamic)Compile-Time (Static) Polymorphism
As the name suggests, the decision is made before the program runs.
The compiler already knows exactly which implementation should be called.
Source Code
β
Compiler
β
Choose Function
β
ExecutableWhen the executable runs, there is no decision left to make.
Everything has already been resolved.
Forms of Compile-Time Polymorphism
C++ provides three primary mechanisms.
Compile-Time Polymorphism
βββ Function Overloading
βββ Operator Overloading
βββ TemplatesEach one represents polymorphism, but in a different way.
Function Overloading
Suppose we write a calculator.
We want one function named
add()But users may add:
- integers
- doubles
- three numbers
- vectors
Instead of inventing new names,
addInt()
addDouble()
addThreeNumbers()we write
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;
}
};Usage:
Calculator calculator;
calculator.add(2, 3);
calculator.add(2.5, 3.7);
calculator.add(1, 2, 3);The interface
add()remains the same.
Different parameter lists produce different implementations.
The compiler decides which one to call. This is compile-time polymorphism.
How Does the Compiler Decide?
Suppose:
calculator.add(10, 20);Compiler reasoning
Arguments
β
(int, int)
β
Best Match
β
add(int,int)No runtime decision exists.
Everything is known during compilation.
Operator Overloading
Operators are simply functions with special syntax.
Normally,
+add integers.
5 + 3But suppose we create a
Complexnumber.
class Complex
{
public:
int real;
int imaginary;
};How should this work?
Complex c1;
Complex c2;
Complex c3 = c1 + c2;The compiler doesn't know.
We teach it.
class Complex
{
public:
int real;
int imaginary;
Complex operator+(const Complex& other)
{
return
{
real + other.real,
imaginary + other.imaginary
};
}
};Now
+has different meanings.
Integers
β
Addition
Complex Numbers
β
Complex AdditionSame operator.
Different implementation.
Compile-time polymorphism.
Template Polymorphism
Templates provide another form.
Suppose we write:
int maximum(int a, int b);Later, users want
double
string
Employee
DateInstead of writing many functions,
we write
template<typename T>
T maximum(T a, T b)
{
return (a > b) ? a : b;
}Usage:
maximum(5, 10);
maximum(2.3, 5.5);
maximum(std::string("A), std::string("B"));The compiler generates specialized code.
Again,
everything happens before execution.
Run-Time (Dynamic) Polymorphism
Now consider a different situation.
Suppose we have
Animal
β
Dog
β
Cat
β
HorseAt compile time, the compiler only knows
Animal*It does not know whether the object will actually be
Dog
Cat
HorseThat decision is made while the program is running.
Program Starts
β
Object Created
β
Pointer Assigned
β
Correct Function SelectedThis is runtime polymorphism.
Example:
class Animal
{
public:
virtual void speak()
{
std::cout << "Animal\n";
}
};
class Dog : public Animal
{
public:
void speak() override
{
std::cout << "Bark\n";
}
};
class Cat : public Animal
{
public:
void speak() override
{
std::cout << "Meow\n";
}
};Output:
BarkLater
animal = new Cat();
animal->speak();Output:
MeowNotice, the code
animal->speak();never changes. Only the object changes.
Why Can't the Compiler Decide?
Suppose:
Animal* animal;Compiler asks
Will this point to
Dog?
Cat?
Horse?
Tiger?It cannot know.
Maybe
animal = loadANimalFromDatabase();Maybe
animal = createAnimalBasedOnUserInput();Maybe
animal = receiveAnimalOverNetwork();The object depends on runtime information.
Therefore the decision must wait until runtime.
Static vs Dynamic Polymorphism
| Feature | Compile-Time | Run-Time |
|---|---|---|
| Decision Time | Compilation | Execution |
| Mechanism | Overloading, Operators, Templates | Virtual Functions |
| Performance | Faster | Slightly Slower |
| Flexibility | Less | More |
| Uses Inheritance | No | Yes (typically) |
Uses virtual | No | Yes |
Leave a comment
Your email address will not be published. Required fields are marked *
