Template-metaprograming or constexpr, a primer and comparison in C++17, part 2

JC
6 min readMar 10, 2023

--

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 Twithout 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 constexprto 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

--

--

JC

SW developer but above all, lover of literature, history, philosophy, arts. Humanist and European trotter.