Template-metaprograming or constexpr, a primer and comparison in C++17, part 2
In C++, SFINAE (Substitution Failure Is Not An Error) and constexpr are two important concepts that play a key role in template metaprogramming and compile-time evaluation, respectively. They both enable the creation of more powerful and efficient code, but they have different use cases and limitations. In this article, we will explain these concepts in-depth, provide code examples, and compare and contrast their strengths and weaknesses.
SFINAE
SFINAE is a mechanism that allows a compiler to exclude functions from the overload set based on whether or not a certain expression can be evaluated. For example, we can use SFINAE to create a template function that calculates the factorial of a number, but only for integral types:
In this code, we are using std::enable_if
to conditionally enable or disable the function template based on whether T
is an integral type. If T
is not integral, std::enable_if
will result in substitution failure, and the compiler will remove this function from the overload set. This will prevent any errors from occurring during compilation, since the function will not be instantiated.
We can also use SFINAE to create more complex template functions that depend on multiple conditions. For example, we can create a template function that only accepts types that have one or more specific member functions. Before doing this , we need just to understand the std::declval utility. It allows one to create temporary objects of a certain type without a default constructor. Example from cppreference.com that I pasted in the compiler explorer:
As per above you can see that the struct NonDefault
has the default destructor deleted! We want to compare that the types of the latter and the struct Default
return are the same when executing the method foo
. Default
has an implicit default destructor , so we create a temporary Default
and call foo
on it. We pass that to decltype to deduct the type and declare a variable n1 of that type (int) to be equal to 1. Now we want to check if foo
in the NonDefault struct returns the same type but since it has no default constructor, as we discussed above we resort to std::declval here that will returns us a rvalue reference from NonDefault
and make it possible to call foo on the temporary object. We do the same then with std::decltype and assign the variable n1
to our newly declared variable n2
. The std::cout
will print the values 1 for both variables.
Now let’s go to the example itself with std::enable_if that checks if we have 2 methods called begin()
and end()
:
In this code, we are using std::declval
which allows one to create temporary objects of type T
without default constructor, and use decltype
to determine the type of their begin()
and end()
member functions. We need to use decltype in order to use declval, otherwise this is not allowed…
We are then using std::is_same
to check whether these types are the same. If they are, the function template is enabled, otherwise it is excluded from the overload set.
One limitation of SFINAE is that it only works during template argument deduction, which means that it cannot be used to conditionally exclude functions after the overload set has already been created. This is where if constexpr
comes in.
if constexpr
if constexpr
is a new feature introduced in C++17 that allows us to conditionally execute code at compile time, based on a compile-time constant expression. Unlike SFINAE, if constexpr
can be used to exclude code from being compiled altogether, even after the overload set has been created.
For example, we can use if constexpr
to create a template function that calculates the factorial of a number, but only for integral types:
In this code, we are using if constexpr
to conditionally execute code based on whether T
is an integral type. If T
is integral, the function will execute the factorial calculation. Otherwise, it will fail a static assertion, which will prevent the code from being compiled. Note that std::is_integral_v
is a shorthand for std::is_integral<T>::value
.
We can also use if constexpr
to create more complex template functions that depend on multiple conditions. For example, we can create a template function that only accepts types that have both a begin()
and end()
member function like we did above with std::enable_if :
In the above code sample, we are creating a type trait has_begin_end
that checks if a type T
has both begin
and end
member functions. we use std::declval
to convert the type T
to a reference type, which allows us to call member functions inside decltype
expressions without having to construct an object as discussed previously. Firstly we call the member function begin()
on std::declval<T>()
, and if that succeeds we discard the result using the comma operator. Next we try to call end()
on std::declval<T>()
, which, if it succeeds we get the return type of using decltype
. Note that decltype
is a must since one can only call member functions on reference types using declval
inside decltype
. Finally, if all of this succeeds, we use void_t
to return void
, otherwise the template parameters of void_t
fail to evaluate and the specialization cannot be resolved during name lookup.
One ubiquitous example that we can use too to contrast the usages of SFINAE and constexpr, it’s to implement the factorial algorithm
Example with SFINAE
In above code, we are using SFINAE to exclude non-integral types from the overload set of the factorial
function by using std::enable_if
with the std::is_integral
to validate it’s a number.
Example with if constexpr
We are using if constexpr
to conditionally execute the factorial calculation code if T
is an integral type. If T
is not an integral type, the function will fail with static assertion, which will prevent the code from being compiled.
Use Cases and Recommendations
SFINAE and if constexpr are both powerful tools that can be used to create more efficient and expressive code. However, they have different use cases and limitations.
SFINAE is best used when you want to conditionally exclude functions from the overload set based on the properties of the function arguments. For example, you can use SFINAE to create function templates that only accept certain types, or that have certain properties. SFINAE is also useful when you want to create more complex template functions that depend on multiple conditions.
if constexpr
, on the other hand, is best used when you want to conditionally execute code at compile time, based on compile-time constant expressions. if constexpr
is especially useful when you want to create more concise and readable code, since the condition is part of the function body instead of being spread out over the template parameters. if constexpr
is also useful when you want to provide more informative error messages, since static assertions can be used to provide more detailed information.
When deciding whether to use SFINAE or if constexpr, consider the following:
- Use SFINAE when you want to conditionally exclude functions based on the properties of the function arguments.
- Use if constexpr when you want to conditionally execute code based on compile-time constant expressions.
- Use SFINAE when you want to create more complex template functions that depend on multiple conditions.
- Use if constexpr when you want to create more concise and readable code, or when you want to provide more informative error messages.
- Consider the limitations of each approach, such as the fact that SFINAE only works during template argument deduction, or that if constexpr requires C++17 support.
Conclusion
SFINAE through the usage of std::enable_if and if constexpr are two important concepts in C++ that enable the creation of efficient and expressive code. SFINAE is best used when you want to conditionally exclude functions based on the properties of the function arguments, while if constexpr is best used when you want to conditionally execute code based on compile-time constant expressions. We learnt we could use declval with delctype to check for the existence of specific member functions when the type lacked a default constructor. By understanding the differences between the SFINAE and if constexpr approaches and their respective strengths and weaknesses, you can make better decisions when writing template functions and creating more efficient and expressive code.
Note: For part 1, check https://medium.com/@joao_vaz/template-meta-programming-in-c-17-primer-1b493a22d51
Picture from <a href=”https://www.freepik.com/free-vector/business-decisions-concept-illustration_11392273.htm#query=choice&position=26&from_view=search&track=sph">Image by storyset</a> on Freepik