Introduction to Polymorphism: Unveiling the Magic Behind Different Effects from “Same” Code
Engineering
Nov 28, 2023

Polymorphism, a concept that powers many programming design patterns, is fundamental and useful in software engineering. It allows for different effects to be produced from the “same” code without the wand, hat, and rabbit of your usual magic shows.
Although mostly related to object orientation, polymorphism can be realized outside the OOP realm.
How does this work and how can I use it, you may ask?
We will talk about two popular ways and the pros and cons of each. For demonstration purposes, we will be using the C++ language.
Runtime Polymorphism
The first is runtime polymorphism, which allows different implementations to be swapped out at runtime. This is the way OOP normally goes, and different languages have distinctive features to get the job done. Some use abstract classes, abstract methods, interfaces, traits, etc. In C++, we use virtual classes and methods. Here is an example:
#include <string>
#include <iostream>
struct Engine
{
virtual std::string get_fuel() const = 0;
};
struct PetrolEngine : public Engine
{
std::string get_fuel() const override { return "petrol"; }
};
struct DieselEngine : public Engine
{
std::string get_fuel() const override { return "diesel"; }
};
void summary(const Engine& engine)
{
std::cout << "This engine uses " << engine.get_fuel() << '\n';
} int main()
{
PetrolEngine p_engine{};
DieselEngine d_engine{};
summary(p_engine);
summary(d_engine);
return 0;
}
Because both the `PetrolEngine` and `DieselEngine` classes are inherited from Engine, either of them can be used where Engine is required. The output of the `summary` function therefore changes based on the supplied argument without hiring the service of control statements.
The benefit of this approach is the ability of a program’s behavior to be reconfigured while the program runs. To make this work though, functions and methods must be called indirectly and consequently, this incurs a performance cost.
Compile Time Polymorphism
The second is compile time polymorphism. In this way, the different implementations and where they are used must be known when the program is being built. As a result, there is no indirection in calling functions and methods, so the performance penalty is not incurred. However, the need for the compiler to know the various implementations and where they are used increases the duration it takes to build the program. As with its runtime counterpart, different languages have different ways compile time polymorphism can be realized. In our case (C++), we use function and class templates.
#include <string>
#include <iostream>
struct PetrolEngine
{
std::string get_fuel() const { return "petrol"; }
};
struct DieselEngine
{
std::string get_fuel() const { return "diesel"; }
};
template <typename T>
void summary(const T& engine)
{
std::cout << "This engine uses " << engine.get_fuel() << '\n';
}
int main()
{
PetrolEngine p_engine{};
DieselEngine d_engine{};
summary(p_engine);
summary(d_engine);
return 0;
}
A similar result of different outcomes based on the input is realized here but with a different technique, and as said in the beginning, no magic was involved.
Which is better, you may ask?
As with everything in engineering (and life 😅), every decision comes with its tradeoffs. You either get a highly configurable program with runtime polymorphism and incur a performance cost or go with the more performant compile time option with potentially longer build times. Although the cons of each of them sound a bit scary, I must say that they only really become a problem in very specific contexts, like in performance-critical applications.
This leads us back to the good old saying, “Use the right tool for the right job and only worry about optimizations when you need to”.



