Polymorphism is a principle in programming where the same expression can have multiple forms, based on the context in which it is used, or the data types it is used with.
This approach can keep our code simple and readable, even as our program gets more and more complex. In C++, polymorphism manifests mainly in three forms:
This includes both operator and function overloading.
A + B
will do different things based on the types of A
and B
.CalculateArea(MyObject)
to behave differently based on the type of MyObject
.Templates allow us to define recipes for blocks of code. The compiler can then use these recipes to generate functions or entire classes that adapt to our needs.
We cover templates in more detail in the next course.
Run Time Polymorphism, which we will delve into later in this chapter, interacts with the inheritance trees, pointers, and references we introduced earlier. It allows our code to behave much more dynamically, without introducing excessive complexity.
To overload functions, we simply define functions within the same scope, that have the same name, but have a different parameter list.
Below, we define a Rectangle
and Circle
type. Alongside each class, we provide standalone CalculateArea()
functions:
struct Rectangle {
float Width;
float Height;
};
float CalculateArea(const Rectangle& R){
return R.Width * R.Height;
}
struct Circle {
float Radius;
};
float CalculateArea(const Circle& C){
return 3.14 * C.Radius * C.Radius;
}
When these functions are both available, they’re considered overloaded.
Elsewhere, in code that uses our classes, we can use a consistent syntax. We can just pass an object to CalculateArea(SomeObject)
, and that expression adapts based on the type of SomeObject
.
From the perspective of the consumer, it doesn’t look like they’re calling two different functions, rather it seems like they’re calling a single, polymorphic function:
int main(){
Circle MyCircle{2};
cout << "Circle Area: "
<< CalculateArea(MyCircle);
Rectangle MyRectangle{3, 4};
cout << "\nRectangle Area: "
<< CalculateArea(MyRectangle);
}
Circle Area: 12.56
Rectangle Area: 12
After running the following code, what is the value of MyBool
?
bool IsFloat(float Number) { return true; }
bool IsFloat(int Number) { return false; }
bool MyBool { IsFloat(5) };
Member functions can also be overloaded in the same way. Let's imagine we have the following situation, where our Character
objects can equip weapons:
class Weapon{};
class Character {
public:
void Equip(Weapon* Weapon){
mWeapon = Weapon;
}
private:
Weapon* mWeapon{nullptr};
};
int main(){
Character Player;
Weapon WoodenSword;
Player.Equip(&WoodenSword);
}
We’d like to be able to equip other items too. Our first instinct to accommodate this might be to rename our existing Equip()
function to EquipWeapon()
, and then add new functions called EquipArmor()
, EquipShield()
, and so on.
But we don’t need to - we can just overload the Equip()
function, keeping our class friendly to use:
class Weapon {};
class Shield {};
class Character {
public:
void Equip(Weapon* Weapon){
mWeapon = Weapon;
}
void Equip(Shield* Shield){
mShield = Shield;
}
private:
Weapon* mWeapon{nullptr};
Shield* mShield{nullptr};
};
int main(){
Character Player;
Weapon WoodenSword;
Player.Equip(&WoodenSword);
Shield WoodenShield;
Player.Equip(&WoodenShield);
}
When we’re working with overloaded functions, we may see the compiler report errors, claiming our function calls are ambiguous.
In these scenarios, we often need to be more explicit about our data types.
An example of a simple program that falls foul of this rule is below:
void Func(int Arg){}
void Func(float Arg){}
int main(){
Func(1.5);
}
'Func': ambiguous call to overloaded function
In an earlier note covering the concepts of literals, we pointed out that a literal like 1.5
is interpreted as a double
, rather than a float
. Float literals append an f
, as in 1.5f
.
So far, we have omitted the f
to make our code easier to read for beginners, and because it generally doesn’t matter - doubles can be implicitly converted to floats.
However, doubles can be implicitly converted to integers, too, which causes a problem in this context.
Without the f
, our code is ambiguous. Should the compiler convert the double
to an int
and call Func(int)
? Or should it convert it to a float
and call Func(float)
?
Rather than guessing our intentions, it asks us to clarify.
In the next lesson, we’ll see how we can solve problems like this by being explicit about our conversions. For now, we can do it manually:
void Func(int Arg){}
void Func(float Arg){}
int main(){
// Call Func(int)
Func(1);
// Call Func(float)
Func(1.5f);
}
After running the following code, what is the value of MyBool
?
bool IsDouble(double Number) { return true; }
bool IsDouble(float Number) { return false; }
bool MyBool { IsDouble(5) };
In this lesson, we delved into several key concepts:
Character
class that can equip different items like weapons and shields.In our upcoming lesson on casting, we will explore type conversion in more depth. Here's what you can expect:
This lesson provides an in-depth look at function overloading in C++, covering its importance, implementation, and best practices
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way