Quick summary β¬ In this blog post, we will learn advanced template programming concepts. These concepts include how to change the implementation of classes and functions based on the type provided, how to work with different arguments and how to properly forward them, how to optimize the code in both runtime and compile-time, about SFINAE, std::enable_if, std::enable_if_t.
It is hard to write template programs because we assume that it works in one way but the code works in the other way. This can only be eradicated by properly understanding what template programming is and some advanced concepts in it.
Recipe of this blog post:
- What is template programming? Why does it even exist?β‘οΈ
- Understanding SFINAEβ‘οΈ
- Understanding and using std::enable_ifβ‘οΈ
1. What is template programming? Why does it even exist?
We create classes and methods but without the use of template programming, we narrow down the usage of it to just one data type. For every single data type the code needs to handle, we need to create a clone of the same code, the only difference is the data type is different. This is unreasonable and it creates lots of headaches in large codebases especially for library developers. This problem gave birth to template meta-programming.
We create templates without even knowing what types are going to be passed to them and what to expect from them. This often leads to unreliable and buggy codebases. What are we going to do about it? We need to explicitly tell the compiler to disable certain pieces of code for certain types. Let me stop telling everything by words and let’s jump into the code right away to explain why we need to explicitly tell the compiler to disable certain code pieces for certain types.
example1.cpp
|
|
The preceding example declared a template for the class MyClass
that has a function named exampleMethod
, which is overloaded to accept an l-value and an r-value references. The above class won’t compile when T is a reference. Because, r-value reference assumes that the argument does not have its own address. So, the code void exampleMethod(T&& x)
throws an error.
|
|
If we have a way to tell the compiler to disable the void exampleMethod(T&& x)
method then we win the game. There is a way. That’s SFINAE.
2. Understanding SFINAE
SFINAE stands for Substitution failure is not an error. It helps us to disable certain pieces of code that don’t accept the type that we pass. In other words, it is a way for the programmers to tell which type will activate the method defined in the class. Let’s jump right in and demystify the SFINAE. I did few modifications in the above example code that incorporates the concept of SFINAE, so the code runs for any type.
example2.cpp
|
|
The preceding example shows an innovative way to disable the methods when it doesn’t fit the type it recieves. One of the exampleMethod
method accepts l-value reference as an argument and the other accepts r-value reference as an argument. When we pass l-value reference, the overloaded exampleMethod that accepts an r-value reference is disabled. This is done by declaring a struct and initializing a type with value int&. In one of the exampleMethod overload, it specifically looks for argument to which int&
is passed in: (lValueRef::lValueRefType = int&). When this happens, rValueRef::rValueRefType is not initialized so the overloaded exampleMethod knew that the method copy that accepts l-value reference must be activated and all the other methods are disabled. In other words, we can tell, substitution has been failed for all the other methods. As the name of this concept defines, Substitution Failure Is Not An Error. The most important takeaway is the compiler was able to pick between the two versions of exampleMethod.
It all works fine. But, Don’t you think this is an ugly way of doing template programming. Continue ready to learn better ways to do the same thing efficiently.
3. std::enable_if and std::enable_if_t - a compile-time switch for templates
enable_if was standarized in C++11 and it was part to Boost library long before that. It helps us to efficiently force the compiler to create the substitution error and pick a different version of the template function. std::enable_if is defined as follows:
|
|
The first enable_if is a genaralized one and the second enable_if struct is more of a specialized version of it. When the boolean value for B is true then the type that is passed to it is returned back or else void will be returned as specified in the genaralized implementation of enable_if.
3.1 Custom-made functions as a first argument to enable_if
example3.cpp
|
|
The preceding example shows the use of enable_if. The first parameter in the angular bracket calls the method is_int and returns true when the type passed is an integer. If not, it returns false. If it returns true then the value of T is passed to the method display_output and it will be executed. Or else, it throws an error and in this case it is an substitution failure and it is not an error. If the T type is not int, std::enable_if turns into nothing. Assigning 0 to nothing results in compilation error.
Check out how the compiler looks at the above template code after assigning the template argument parameters.
|
|
3.2 Standard library function as a first argument to enable_if with return type as a int pointer
Let’s look into an another example:
example4.cpp
|
|
The preceding variation is another example of enable_if that accepts standard library defined function to return back the boolean value. If the type we pass is an integer then std::is_integral
Check out how the compiler looks at the above template code:
|
|
3.3 Using enable_if as a return type of the function
example5.cpp
|
|
In the above example, if T is not an integer then void is not returned by the enable_if. Thus the return type of display_output becomes nothing instead of void . And, that leads to compilation error which is nothing but a substitution failure.
3.4 How to eradicate ::value, ::type and typename and make the code simple to read?
All the above examples looks ugly and it’s definitely not looking beautiful. Let’s make it beautiful so you can embrace the benefit of the compile time switch enable_if .
We knew that enable_if was introduced with boost library many years back and it was first added to standard library in C++11. A variant of enable_if was introduced in C++14 and it’s compliant with C++11. You just need to add the below code if you haven’t upgraded your codebase to C++14 or later versions. Below is the code for the new variant:
|
|
The above code helps us to write simple and neat template programming. Our example SFINAE can be reduced from this:
|
|
To the below code:
|
|
std::is_integral
|
|
Further improvements: Instead of appending _v to the prefix instead of ::value to the std::is_integral, it’s better to have {} added to the prefix after the angular brackets as shown in the below code.
|
|
This instantiates a value to std::is_integral
Let’s again consider the example1.cpp and improve it with all the learning we accumulated. To remind, The below code is the example1.cpp:
|
|
Now, we can make it beautiful with the learning we had and here is the final code shown below:
example6.cpp
|
|
Here is the result of the example6.cpp program:
|
|