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.
There are two versions of the template function
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 #2template 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.
- If the parse set is empty, then the compilation fails, e.g. if we remove the
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.
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.
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.
The above represents the two usual ways of using
- the return value type uses
- 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.
To get the
(value==false) argument to pass you also need to add a template.
Let’s see how
enable_if can do this using the SFINAE principle.
You can see that when the first type of
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
true is inverted.
In addition there is a template for enable_if_t.
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.
decltype also has a post-return type syntax that combines decltype and auto to complete the derivation of the return value type.
However, sometimes a class may not have a default constructor, in which case the above methods cannot be used, e.g.
std::declval comes in handy.
So by using decltype & std::declval we can make the above example a bit simpler to write.
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.
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.
This type template will map arbitrary types to void, so for the has_reserve function we mentioned above, you can write it like this.
Using decltype, declval and template specialization above, we have greatly simplified the definition of has_reserve. Here’s how we can write it.
下面我们看看 void_t 是怎么生效的。
new_has_reserve< A >::value去匹配的时候，肯定是符合的，相当于
new_has_reserve< A, void >::value。
Let’s take a look at another template match.
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.
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 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.
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.
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.
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.