We’ve previously seen the benefits of polymorphism.
When we have a variable or function parameter that is a pointer or a reference to a type, objects of any class that inherits from that subtype can be used.
In this example, we have a Taunt
function that accepts a reference to a Character
. We can pass an object of a type of Orc
to this function.
This works because Orc
inherits from Character
, therefore all Orcs are also Characters:
class Character {};
class Orc : public Character {};
void Taunt(Character& Character) {
// ...
};
int main() {
Orc Basher;
Taunt(Basher);
}
We’ve also seen how we can use virtual functions to override the behavior of those base implementations. This allows our function to behave differently based on the specific subtype of Character it receives. This is the essence of polymorphism:
#include <iostream>
using namespace std;
class Character {
public:
virtual string GetTaunt() {
return "???";
}
};
class Orc : public Character {
public:
virtual string GetTaunt() override {
return "Come get some!";
}
};
void Taunt(Character& Taunter) {
cout << Taunter.GetTaunt() << endl;
};
int main() {
Orc Basher;
Taunt(Basher);
}
Come get some!
This allows our code to manage complexity in an elegant way. We could have hundreds of different character subtypes, each with unique behaviors. But, that is all hidden away within their respective class code - our Taunt
function never needs to change.
However, a common problem arises with this design: the function we want to call needs to exist on the base class, but it’s not obvious what that function should do or return.
The Character
class only really exists as a way to create a standardized interface among all its subtypes. We never expect to actually create base Character
objects - we’re always going to create more specific objects.
So, in this scenario, GetTaunt
on our Character
class can be a pure virtual function.
To create a pure virtual function, we assign it a value of 0
:
class Character {
public:
virtual string GetTaunt() = 0;
};
After making this change, Character
is now an abstract class. Abstract classes have two properties:
Character
objects will now need to be created from more specific subclassesCharacter
does not override this function to provide an implementation, that class will also be abstract// Abstract - GetTaunt is pure virtual
class Character {
public:
virtual string GetTaunt() = 0;
};
// Abstract - GetTaunt is pure virtual
class Fish : public Character {};
// Not Abstract - GetTaunt is implemented
class Orc : public Character {
public:
virtual string GetTaunt() override {
return Character::GetTaunt();
}
};
int main() {
// Cannot create objects from abstract classes
Character Henry;
Fish Flappy;
// Can only create objects from non-abstract classes
Orc Basher;
}
Typically, classes that are not abstract are referred to as “concrete”. So, in the above example, Orc
is a “concrete class”.
There is an additional technique that allows pure virtual functions to have an optional implementation. It looks like this:
class Character {
public:
virtual string GetTaunt() = 0;
};
string Character::GetTaunt() {
return "Default taunt!";
}
Here, Character
is still an abstract class, but, for classes that inherit from Character
, a default implementation of this function is available if they want to use it. If they want to use it, they need to explicitly call it:
class Human : public Character {
public:
virtual string GetTaunt() override {
return Character::GetTaunt();
}
};
Typically, the motivation for doing this is that we expect most subclasses will need to override this function. If it’s not overridden, we want the class to explicitly confirm they’re happy with the default. This mitigates scenarios where a developer wasn’t aware that something normally needs to be overridden, or they simply forgot to do so.
Many other object-oriented programming languages have the concept of interfaces. Interfaces specify a set of requirements for an object but do not provide any code to implement those requirements.
C++ does not have a formal implementation of interfaces, but the same behavior can be achieved using abstract classes. A class that has no variables, and only pure-virtual functions, is effectively an interface. By convention, classes that are intended to be used as interfaces have an I
at the start of their name:
class ITaunter {
public:
virtual string GetTaunt() = 0;
}
This gives us an alternative way to implement our design. Now, our GetTaunt
function no longer needs to accept a Character&
, it can instead accept an ITaunter&
:
void Taunt(ITaunter& Taunter) {
cout << Taunter.GetTaunt() << endl;
};
This frees us from needing to define GetTaunt
in our Character
base class. It also gives us more flexibility in our design. All characters can still be Character
objects, but now, each class can decide whether or not that specific subtype is capable of performing this additional action.
It does that by inheriting from and implementing the requirements of ITaunter
. Once a class does that, its objects are then able to be passed to the GetTaunt
 function.
#include <iostream>
using namespace std;
class Character {};
class Fish : public Character {};
class ITaunter {
public:
virtual string GetTaunt() = 0;
}
class Orc : public Character, public ITaunter {
public:
virtual string GetTaunt() override {
return "Come get some!";
}
};
void Taunt(ITaunter& Taunter) {
cout << Taunter.GetTaunt() << endl;
};
int main() {
// A Character that can taunt
Orc Basher;
Taunt(Basher);
// A Character that cannot taunt
Fish Flappy;
Taunt(Flappy); // Error!
}
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.