SFINAE is actually overloaded function template matching, where the compiler finds all applicable functions and function templates based on their names, and then has to replace the template formal parameters according to the actual situation, in the process of compiling to find a best match.

For example, the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Test {
    typedef int foo;
};

template <typename T>
//Requires type T to define the inline type foo
void f(typename T::foo) {} // Definition #1

template <typename T>
void f(T) {}               // Definition #2

int main() {
    f<Test>(10); // Call #1.
    f<int>(10);  // Call #2. Without error (even though there is no int::foo) thanks to SFINAE. 
    return 0;
}

There are two versions of the template function f defined. f<int> passes in the int type, so it can only be adapted to #2, while f<Test> passes in the Test type, which is defined inside the Test structure, so it can be adapted to #1.

Then when the compiler tries to compile a function call it actually does these things.

  • the first is a lookup based on the execution name.
  • For function templates, the template parameters are inferred based on the types of the incoming parameters.
    • After finding the corresponding template based on the incoming type a parameter type substitution is performed and added to the parse set.
    • If a corresponding type is found that does not match then it is removed from the parsing set.
  • At the end, we can get the parse set for this function call.
    • If the parse set is empty, then the compilation fails, e.g. if we remove the Definition #2 template from the above example, then the f<int>(10); call will fail to compile.
    • If the parse set has more than one, then the most appropriate function that can be matched needs to be found based on the argument type.

The compiler tries to compile a function call

Then the SFINAE rules can be used to make some compile-time decisions, such as whether the class defines an inline type, whether a member function with a given name is defined, etc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template <typename T>
struct has_reserve {
    struct good { char dummy; };
    struct bad { char dummy[2]; };

    template <class U, float (U::*)()>
    struct SFINAE {};

    template <typename  U>
    static good test(SFINAE<U, &U::reserve>*);

    template <typename>
    static bad test(...);

    static const bool value = sizeof(test<T>(nullptr)) == sizeof(good);
};

class  TestReserve {
public:
    float reserve();
};

class Bar {
public:
    int type;
};

int main() {   
    cout << "reserve:" << has_reserve<TestReserve>::value << endl;//reserve: 1
    cout << "reserve:" << has_reserve<Bar>::value << endl; //reserve:  0
    return 0;
} 

We define a SFINAE template, again with insignificant content, but with the second argument being a pointer to the member function of the first argument, with an empty argument type, and a float return value. then we define a reserve member function template that requires the SFINAE* type, with a good return value, and another one that requires no argument type Then we define a template for a reserve member function that does not require a parameter type.

We define a constant integer boolean value, and whether the result is true or false depends on whether nullptr can successfully match SFINAE*, which in turn depends on whether the template parameter T has a member function reserve that returns a type void, takes one argument, and is of type size_t.

If T does not define a reserve, such as Bar, the compiler continues to adapt the second one after the first one fails and succeeds, returning bad due to the SFINAE principle.

enable_if

In C++ 11 enable_if appears, it is a toolset that makes SFINAE easier to use, first look at the two uses of enable_if from the cppreference example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 1. the return type (bool) is only valid if T is an integral type:
template <typename T>
typename std::enable_if<has_reserve<T>::value,void>::type
  reserve_test1 () {cout << "reserve_test1"<< endl;}

// 2. the second template argument is only valid if T is an integral type:
template < typename T,
           typename = typename std::enable_if<has_reserve<T>::value>::type>
void reserve_test2 () {cout <<"reserve_test2" << endl;}

int main() {   
    reserve_test1<TestReserve>();
    reserve_test2<TestReserve>();
    return 0;
} 

The above represents the two usual ways of using enable_if.

  1. the return value type uses enable_if
  2. the template parameter additionally specifies a default parameter class = typename std::enable_if<...>::type

The advantage of using enable_if is to control the function to accept only certain types of (value==true) arguments, otherwise the compilation reports an error, for example if we add this sentence: reserve_test1<Bar>(); it will report an error and cannot find the corresponding type.

1
error: no type named 'type' in 'struct std::enable_if<false, void>'

To get the (value==false) argument to pass you also need to add a template.

1
2
3
4
5
6
7
8
template <typename T>
typename std::enable_if<!has_reserve<T>::value,void>::type
  reserve_test1 ()  {cout << "is not reserve " << endl;}

int main() {   
    reserve_test1<Bar>();
    return 0;
} 

Let’s see how enable_if can do this using the SFINAE principle.

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

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

You can see that when the first type of enable_if is true, it will specialize to the second implementation, and the embedded type type will exist. Otherwise the compiler matches the first implementation and the embedded type type does not exist, which is why the above compilation action prompts. So when we add an is_odd overload template, when std::is_integral is determined to be false, it will still specialize to the second implementation of enable_if when true is inverted.

In addition there is a template for enable_if_t.

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

enable_if_t is the redefinition of enable_if::type, if the Test of enable_if_t<_Test,_Ty> is true, we can see that we have entered the special version of enble_if with the definition of type, otherwise we don’t have the definition of type, with this version In fact, we can remove the ::type from the above example and simplify the code a little, which can be useful in many places.

decltype & std::declval

The introduction of decltype & std::declval in C++ 11 has brought a lot of convenience to template programming, here is a brief introduction to both of them.

decltype is short for “declare type”. decltype can be thought of as the same as the auto keyword for compile-time type derivation, but instead of obtaining the type of a variable from the initialization expression of the variable declaration, as auto does, decltype always takes an ordinary expression as an argument and returns the type of that expression. Instead, it always takes a common expression as an argument and returns the type of that expression, and decltype does not evaluate the expression. A simple usage is as follows.

1
2
int i = 4;
decltype(i) a; //The result of the derivation is int. a is of type int.

decltype also has a post-return type syntax that combines decltype and auto to complete the derivation of the return value type.

1
2
3
4
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u){
    return t + u;
}

However, sometimes a class may not have a default constructor, in which case the above methods cannot be used, e.g.

1
2
3
4
5
6
7
8
struct A {
    A() = delete;
    int foo();
};

int main() {
    decltype(A().foo()) foo = 1; // Cannot pass compilation
}

So std::declval comes in handy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <utility>

struct A {
    A() = delete;
    int foo();
};

int main() {
    decltype(std::declval<A>().foo()) foo = 1; // OK
}

So by using decltype & std::declval we can make the above example a bit simpler to write.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename  U>
auto test() ->decltype(declval<U&>().reserve(),void())
{
    cout << "type " << endl;
}

int main() {   
    test<TestReserve>();
    return 0; 
} 

declval can be used to hypothetically derive an object of a type without a default constructor for that type. So declval<U&>().reserve() is used to test if an object of type U& has a reserve member function.

Note that a comma expression in C++ is meant to be evaluated sequentially, one by one, and return the last item. So the second argument of decltype means that the return value is of type void.

void_t

In C++ 17 we can also use void_t together with decltype and declval to implement the above function of enable_if.

The definition of void_t is as follows.

1
2
template <typename...>
using void_t = void;

This type template will map arbitrary types to void, so for the has_reserve function we mentioned above, you can write it like this.

1
2
3
4
5
6
7
template< class , class = void >
struct new_has_reserve : std::false_type
{ };

template< class T >
struct new_has_reserve< T , void_t< decltype(declval<T&>().reserve() ) > > : std::true_type
{ };

Using decltype, declval and template specialization above, we have greatly simplified the definition of has_reserve. Here’s how we can write it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class A {
public:
    int reserve();
};

class B {
};

int main() {   
    cout << new_has_reserve< A >::value << endl; // 1
    cout <<new_has_reserve<B>::value << endl; // 0
    return 0; 
} 

下面我们看看 void_t 是怎么生效的。

首先对于这个模板来说,它的模板参数列表有两个,第二个模板参数如果不填的话,那就是默认的 void,所以当 new_has_reserve< A >::value去匹配的时候,肯定是符合的,相当于 new_has_reserve< A, void >::value

1
2
3
template< class , class = void >
struct new_has_reserve : std::false_type
{ };

Let’s take a look at another template match.

1
2
3
template< class T >
struct new_has_reserve< T , void_t< decltype(declval<T&>().reserve() ) > > : std::true_type
{ };

When new_has_reserve< A >::value goes to match, T is deducible to A for the first template argument.

For the second argument you can actually write void_t< decltype(declval<A&>().reserve() ), declval as we have talked about above, it can hypothetically derive an object of the class for type derivation if there is no default constructor for a certain type, so declval<A&>().reserve() actually looks at whether A has a reserve function, and if it does, it uses decltype to try to get the type of the reserve function, which is then replaced by void_t with void type, all with no problem, and the result is actually derived as follows.

1
2
3
template< class T >
struct new_has_reserve< A , void ) > > : std::true_type
{ };

That means both templates can be matched successfully, and then the compiler will pick a biased specialization as the most suitable template to match.

constexpr

constexpr It was introduced in C++ 11 and it literally means constant expression. It can act on variables and functions. A constexpr variable is a constant that is fully determined at compile time. A constexpr function can produce a compile-time constant during compilation for at least a certain set of real parameters.

Note that const does not distinguish between compile-time and run-time constants, and that const only guarantees that it will not be modified directly at run-time, whereas constexpr is restricted to compile-time constants. After C++11, it is recommended to use constexpr for all constant semantics, and const only for read-only semantics. e.g.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<int N> class C{};

constexpr int FivePlus(int x) {
  return 5 + x;
}

void f(const int x) {
  C<x> c1;            // Error: x is not compile-time evaluable.
  C<FivePlus(6)> c2;  // OK
} 

C++17 & if constexpr

In C++17, the syntax if constexpr was added to make template programming more readable.

Let’s look at an example. In C++11/14, to use the enable_if template mentioned earlier, we usually have to implement two close_enough templates.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <class T> constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T> 
constexpr enable_if_t<is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return absolute(a - b) < static_cast<T>(0.000001);
}
template <class T>
constexpr enable_if_t<!is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return a == b;
}

But in C++17 with syntax like if constexpr it can be simplified to a close_enough template, and the constants can be abstracted out into constexpr variables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template <class T> constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T>
constexpr auto precision_threshold = T(0.000001);

template <class T> constexpr bool close_enough(T a, T b) {
   if constexpr (is_floating_point_v<T>)  
      return absolute(a - b) < precision_threshold<T>;
   else
      return a == b;
}

Using if constexpr the compiler will calculate if the branch is conditional at compile time, and if it is not, it will do an optimization to discard the branch.

Ref

  • https://www.luozhiyun.com/archives/744