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_if
in 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 T is an integer type (int , char , long , etc.) |
std::is_floating_point<T> |
Checks if T is float , double , or long double |
std::is_pointer<T> |
Checks if T is a pointer type (T* ) |
std::is_reference<T> |
Checks if T is a reference type (T& or T&& ) |
std::is_array<T> |
Checks if T is an array |
std::is_class<T> |
Checks if T is a class or struct |
std::is_enum<T> |
Checks if T is an enum |
std::is_same<T, U> |
Checks if T and U are the same type |
std::is_convertible<T, U> |
Checks if T is implicitly convertible to U |
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 betrue
.- 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 = void
parameter allowsstd::enable_if
specializations 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.