SFINAE
“Substitution Failure is Not An Error”
1. Introduction
According to the definition on cppreference, SFINAE rule applies during overload resolution of function templates: When substituting the explicitly specified or deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error. This feature is used in template metaprogramming.
What it means is that:
- When multiple function templates are available, the compiler attempts to instantiate them one by one.
- If one instantiation fails due to an invalid substitution, the compiler simply ignores that function instead of generating a compile-time error.
- The compiler continues searching for another viable function in the overload set.
Here is an example of how SFINAE can preventing compilation errors:
| 1 | 
 | 
What Happens in Overload Resolution?
- foo(42)
- T = int
- std::enable_if_t<std::is_integral_v<int>, void>→ Valid
- Integer overload remains in the overload set.
- Calls the integer overload.
- foo(3.14)
- T = double
- std::enable_if_t<std::is_floating_point_v<double>, void>→ Valid
- Floating-point overload remains in the overload set.
- Calls the floating-point overload.
- foo(“hello”)
- T = const char*
- std::enable_if_t<std::is_integral_v<int>, void>→ Fails (not an integer)
- std::enable_if_t<std::is_floating_point_v<int>, void>→ Fails (not a floating point)
- Both overloads are discarded.
- Compilation error: No matching function found.
SFINAE (Substitution Failure Is Not An Error) can be achieved in multiple ways in C++.
- std::enable_ifin function return type.
- Overloaded functions with std::enable_if.
- Class template specialization.
- Expression SFINAE (decltype).
- concept
Let’s look at each one of them in the next sections.
Before we move on to the next section, it will be helpful to have some basic understanding about common type traits in C++.
| Type Trait | Meaning | 
|---|---|
| std::is_integral<T> | Checks if Tis an integer type (int,char,long, etc.) | 
| std::is_floating_point<T> | Checks if Tisfloat,double, orlong double | 
| std::is_pointer<T> | Checks if Tis a pointer type (T*) | 
| std::is_reference<T> | Checks if Tis a reference type (T&orT&&) | 
| std::is_array<T> | Checks if Tis an array | 
| std::is_class<T> | Checks if Tis a class or struct | 
| std::is_enum<T> | Checks if Tis an enum | 
| std::is_same<T, U> | Checks if TandUare the same type | 
| std::is_convertible<T, U> | Checks if Tis implicitly convertible toU | 
Since std::enable_if is used frequently to achieve SFINAE, it is necessary to first understand its meaning and usage.
std::enable_if is available in <type_traits>. Its possible definition could be:
| 1 | template< bool B, class T = void > | 
Let’s reuse the example above and include our defined enable_if struct.
| 1 | template <typename T> | 
When we call foo(42):
- std::is_integral_v<T>evaluates to be- true.
- When true, we have a typenametype = void.
- The function can be expanded to: void foo(int value), which is a valid signiture.
When we call foo(3.14):
- T = double, which is not an integral type.
- std::is_integral_v<double> == false.
- Since std::enable_if<false, void>has no type member, the function is discarded.
- There is no matching function, so the substitution fails.
2. Function return type
std::enable_if allows us to conditionally enable or disable function templates based on type traits by using its associated type alias.
| 1 | // Helper types | 
| 1 | // Only enable if T is an integral type | 
if T is an integral type, std::enable_if_t<std::is_integral_v<T>, void> resolves to void, which is a valid function signiture. However, if T is a double type, substitution fails, and the function discarded, leading to no matching function call.
3. Function overload
| 1 | template <typename T> | 
4. Template specialization
| 1 | // Primary template (disabled by default) | 
We leave the primary template undefined for two purposes:
- Prevents the use of Foo<T>with unsupported types (such asstd::string).
- The default Enable = voidparameter allowsstd::enable_ifspecializations to work smoothly by using SFINAE.
5. Expression SFINAE
SFINAE can be applied based on whether an expression is valid.
| 1 | template <typename T> | 
decltype(obj.size(), std::true_type{}), if obj.size() is not valid, the compiler can not evaluate and it fails at substitution. This is when SFINEA kicks in and the function is discarded.
6. Using concept (C++20)
In C++20, concepts provide a powerful way to constrain template parameters, offering an alternative to traditional SFINAE techniques. Concepts are more readable and expressive than using std::enable_if or other SFINAE tricks.
A concept is a predicate that defines a set of requirements for a type. You can think of it as a constraint on the template parameters, which is checked at compile time.
Basic usage:
| 1 | template <typename T> | 
Multiple concepts:
| 1 | template <typename T> | 
Combined with requires for more complex constraints. The requires clause in C++20 is used to specify constraints on template parameters. It allows you to check conditions on types or expressions at compile time. This is part of the new concept-based feature in C++20,
| 1 | template <typename T> | 
7. Conclusion
In this post, we discussed some common SFINAE implementations. Of course, there are some other techniques such as std::void_t, default template parameters, etc. Interested readers can continue to explore on these topics.
