Author: Chloé Lourseyre
Let’s say you have a function you have to call using two parameters :
- The first parameter is a configuration option symbolized by an enum. Like the “mode of use” of your function.
- The second parameter is a true numeral parameter.
Let’s use the following example to represent that :
#include <iostream>
// Enum describing the mode used in the following function
enum class ChangeMode
{
Before,
After
};
// Function that squares a value and increases it (wether before or after), then prints the result
void increase_and_square(const ChangeMode& m, int v)
{
if (m == ChangeMode::Before)
++v;
v = v*v;
if (m == ChangeMode::After)
++v;
std::cout << v << std::endl;
}
// main function
int main()
{
const int a = 42;
increase_and_square(ChangeMode::Before, a);
increase_and_square(ChangeMode::After, a);
}
In this example, we perform slightly different operation depending on the “mode” used. This is a fairly common behavior that can occur in may different forms.
However, implemented as it is there is not the most optimized way to do this. A best way to obtain the same result is to use the ChangeMode
as a template instead of a plain parameter.
Like this :
#include <iostream>
// Enum describing the mode used in the following function
enum class ChangeMode
{
Before,
After
};
// Function that squares a value and increases it (wether before or after), then prints the result
template<ChangeMode m>
void increase_and_square(int v)
{
if (m == ChangeMode::Before)
++v;
v = v*v;
if (m == ChangeMode::After)
++v;
std::cout << v << std::endl;
}
// main function
int main()
{
const int a = 42;
increase_and_square<ChangeMode::Before>(a);
increase_and_square<ChangeMode::After>(a);
}
Impementing the function like is actually better in every way :
- Operations done at compile time : of course, and as the main advantage of using templates, we evaluate more expressions a compile time here, saving run time.
- Compiler optimization : since the compiler will be able to evaluate more things at compile time, it will be able to optimize the code, effectively discarding the
if
statements. - Executable size : more impressive, the executable will be smaller with the templated version. This is so because the fact that we discard the if statement actually greatly reduce the size of the functions.
To illustrate this, here is the template instanciation the compiler generates :
void increase_and_square<ChangeMode::Before>(int v)
{
++v;
v = v*v;
std::cout << v << std::endl;
}
void increase_and_square<ChangeMode::After>(int v)
{
v = v*v;
++v;
std::cout << v << std::endl;
}
These are way simpler functions that the big bulky one above.
If you are not convinced, here is the number of assembly instructions the compiler generates (I used Compiler Explorer (godbolt.org) with clang as compiler and --std=c++20 -O3
as compiler options).
Parameter
123
Template
76
The template version is clearly better, faster, prettier.
Perk : double default values
Using the template version of this code also provides another perk : the ability to give a default value to your mode and to your parameter.
Since default values are decorrelated for templates and parameters, you can write this :
#include <iostream>
// Enum describing the mode used in the following function
enum class ChangeMode
{
Before,
After
};
// Function that squares a value and increases it (wether before or after), then prints the result
template<ChangeMode m=ChangeMode::Before>
void increase_and_square(int v = 2)
{
if (m == ChangeMode::Before)
++v;
v = v*v;
if (m == ChangeMode::After)
++v;
std::cout << v << std::endl;
}
int main()
{
const int a = 42;
increase_and_square(a);
increase_and_square<ChangeMode::After>();
increase_and_square();
}
This is interesting because the mode and the parameter actually have different semantic meanings, thus it is only logical that they have separate behavior.
Limitations
Why does this work ? Because the “mode” we template is actually an enum with very few values.
If you happen to use an bigger enum (with 5+ different values) or other types, the template version will be way worse than the parameter version.
In general, you will have no use doing what we saw in this article, but in the very specific case you have an enum that describes a “mode of use” of your function and has 2 or 3 possible values, take time to consider using it as template.
Keep in mind that this model, if seducing, is very specific and will not apply in your day-to-day coding.
Semantic
I will conclude this article by talking a bit about semantics.
What we achieved here could be seen as an overload of the increase_and_square()
function. Indeed, we did several implementations of the same function (well, the template did it for us) that has a similar-yet-different behavior. This is exactly what overloading is about.
This is also why I described the enum as a “mode” : it describes how the function must work, and is more that than a real parameter.
Thanks for your attention and see you next week!
Author: Chloé Lourseyre