C++ Notes - SFINAE & std::enable_if

SFINAE

SFINAE is the abbreviation of “substitution failure is not an error“, that is, matching failure is not an error. That is to say, if matching an overloaded function/class will cause a compilation error, the function/class will not be a candidate and the compiler will look for another overload version. This is a new feature of C++11 and the core principle of enable_if.

Complete Overload Matching Order

  1. Find all candidates and remove those who will cause compile error.
  2. Try to find a viable version:
    • Exact argument type matching is possible.
      1. value -> reference
      2. array -> pointer
      3. function -> function pointer
      4. pointer -> const pointer
      5. pointer -> volatile pointer
    • Argument types can be matched through default parameters.
    • Argument types can be matched through default type conversions.
      1. char/short -> int/float/double
      2. int -> char
      3. long -> double
      4. etc.
    • Argument types can be matched through user-defined type conversions.
      1. class constructors
      2. type conversion functions
      3. etc.
  3. Non-template functions take precedence over template functions.
  4. Matching failed
    1. The final found feasible functions are not unique, resulting in ambiguity and compilation failure.
    2. Unable to match all candidates, the function is undefined, resulting in compilation failure.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class testA
{
public:
int m_Data;
testA(int value):m_Data(value){};
};

class testB : public test A
{
public:
typedef double returnValue;
testB(int value):testA(value){};
}

int add(testA t1, testB t2)
{
return t1.m_Data + t2.m_Data;
}

template<typename T>
typename T::returnValue add(T t1, T t2)
{
return t1.m_Data + t2.m_Data;
}

The code provided appears to have an issue: it seems to use the returnValue type defined in testB when we haven’t explicitly specified the argument type as either testA or testB. It might seem like passing a testA type would cause a compilation error.

However, thanks to the SFINAE mechanism, the compiler treats both add functions as candidates during type deduction. When it encounters a failure (for example, when the input type is testA, but returnValue is not defined in it), the compiler discards that template and proceeds to other functions without producing an error.


std::enable_if

Principle

The definition of std::enable_if is pretty simple:

1
2
3
4
5
6
7
8
9
template<bool, typename T=void>
struct enable_if
{}

template<typename T>
struct enable_if<true, T>
{
using type = T;
}

The former is a regular version of a template class, and the latter is a partial specialization version of a template class.

In that case, std::enable_if<true, T>::type would be T, and &std::enable_if<false, T>::type would cause a compile error. (under SFINAE, the compiler will not consider functions/classes containing this kind of enable_if as a candidate)

1
2
3
4
typename std::enable_if<true, int>::type t;     // correct, equals to : int t;
typename std::enable_if<true>::type; // correct, equals to : void;
typename std::enable_if<false>::type; // failed, there's no definition for type
typename std::enable_if<false, int>:: type t2; // failed, same as above

Usage

std::enable_if could be used anywhere as a typename. It is pretty useful in C++ template meta progamming.

  1. Template Partial Specialization
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
typename std::enable_if<std::is_trivial<T>::value>::type check()
{
std::cout << "T is trivial" << std::endl;
}

template<typename T>
typename std::enable_if<!std::is_trivial<T>::value>::type check()
{
std::cout << "T is not trivial" << std::endl;
}

When you call check<some typename>(), compiler will generate functions called void check() and output “T is (not) trivial” to the console depending on what you put in <>.

  1. Validate Template’s Parameter Types
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
typename std::enable_if<std::is_intergral<T>::value, bool>::type is_odd(T t)
{
return bool(t % 2);
}

template<typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even(T t)
{
return !is_odd(t);
}

In the example above, we are validating the template’s parameter type by two ways: defining the return value and the default template parameter. By defining function like this, only intergral type can be used to call is_odd and is_even.


std::enable_if_t

It is easy to understand std::enable_if_t with std::enable_t. Its definition is:

1
2
template<bool _Test, typename _Ty = void>
using enable_if_t = typename enable_if<_Test, _Ty>::type;

It is simply the alias of std::enable_if<_Test, _Ty>::type. You can use this as a typename directly in the code

1
2
3
4
5
template<typename T>
typename std::enable_if_t<std::is_integral<T>::value, bool> is_odd(T, t)
{
return bool(t % 2);
}

C++ Notes - SFINAE & std::enable_if
https://rigel.github.io/C++Note02/
Author
Rigel
Posted on
October 7, 2023
Licensed under