Back to Programming techniques
16.05.2015
SFINAE — an acronym for Substitution Failure Is Not An Error — is a rule in the C++ standard. The rule states that if the substitution of a template-declaration generates errors, then the declaration is removed from the overload-set, and the errors are ignored.
The SFINAE rule is only in effect when it occurs in a template declaration. That is,
In particular,
The SFINAE rule can be abused to constrain the parameter-types of a function/class template. This modifies which overload-sets the function/class participates in. Suppose we have
template <typename Type>
Type f(Type that);
and would like to constrain f
to only work on native integer types. We can achieve this using SFINAE as follows:
template <
typename Type,
Requires<std::is_integral<Type>> = 0>
Type f(Type that);
Here the Requires
is defined as follows:
template <bool Condition, typename Type = int>
struct RequiresC_F
{
using type = Type;
};
template <typename Type>
struct RequiresC_F<false, Type>
{};
template <bool Condition, typename Type>
using RequiresC =
typename RequiresC_F<Condition, Type>::type;
template <typename Condition, typename Type>
using Requires =
RequiresC<Condition::value, Type>;
When f
is called with something else than a native integer type, the std::integral<Type>::value
evaluates to false
, and the latter template-parameter becomes typename RequiresC_F<false>::type
. Since RequiresC_F<false>
does not contain a type
member, an error occurs. Since the error occurs due to a substitution in a template declaration, the SFINAE rule applies, and the function is removed from the overload set.
An ideal template-constraint-description
decltype
),Currently — in C++14 — such an ideal constraint-description does not exist. Instead, it is approximated by an SFINAE-constraint, which can be embedded into a declaration in many ways, with different trade-offs.
The SFINAE-constraint can be embedded into a return-type:
template <typename Type>
Requires<std::is_integral<Type>, Type> f(Type that);
The disadvantage is the loss of readability in the return-type.
The SFINAE-constraint can be embedded into a trailing return-type:
template <typename Type>
auto f(Type that)
-> Requires<std::is_integral<Type>, decltype(that)>
The advantage is that it is possible to refer to function arguments. The disadvantage is the loss of readability in the return-type.
The SFINAE-constraint can be embedded into a trailing argument-type:
template <typename Type>
Type f(Type that, Requires<std::is_integral<Type>> = 0);
The advantage is that it is possible to refer to function arguments. The disadvantage is the addition of a non-meaningful parameter:
// This is a valid call.
f(4, 1)
Readability is also somewhat affected.
The SFINAE-constraint can be embedded into a template-parameter type:
template <
typename Type,
typename = Requires<std::is_integral<Type>>>
Type f(Type that);
The disadvantage is that this does not allow to overload functions with the same signatures — excluding the constraints. Suppose we later add the following overload:
template <
typename Type,
typename = RequiresC<!std::is_integral<Type>::value>>
Type f(Type that)
{}
Rather than dividing f
into two cases, this redefines the same function template f
; the return-type, the parameters, and the template-parameters are equal, and the default template-parameter does not differentiate the two.
Since such overloads may need to be added later, this technique should be avoided.
The SFINAE-constraint can be embedded into a template-parameter value:
template <
typename Type,
Requires<std::is_integral<Type>> = 0>
Type f(Type that);
The disadvantage is the addition of a non-meaningful template-parameter:
// This is a valid call.
f<int, 1>(4);
The SFINAE-constraint can be embedded into a template-parameter value-pack:
template <
typename Type,
Requires<std::is_integral<Type>>...>
Type f(Type that);
The disadvantage is the addition of a non-meaningful template-parameter-pack:
// This is a valid call.
f<int, 1, 2, 3>(4);
An advantage of this technique over the value one is that the default template-parameter need not be specified. Then the type underlying Requires
(now int
) can be changed to enum class Enabler
— which is declared, but not defined. This makes it impossible for the user to provide template-arguments in the value-pack.
Visual C++ 2015 RC has a bug in the compiler, and cannot deal with the value-pack approach.
Place | Clean declaration | Refer to arguments | Overload | No user extra |
---|---|---|---|---|
Return-type | ||||
Trailing return-type | ||||
Trailing argument-type | ||||
Template-parameter type | ||||
Template-parameter value | ||||
Template-parameter value-pack |
If there is no need to refer to function arguments, a template-parameter value-pack is the best choice. A template-parameter value can be used if the compiler cannot handle a template-parameter value-pack. For referring to function arguments, a trailing return-type is the best choice.
A return-type and a trailing argument-type can be used if everything else fails due to compiler bugs.
A template-parameter type should not be used, because it does not always work with overloading.
In C++14, using the SFINAE rule is the only way to implement template constraints. It was not intended for this task, which shows in that it can make code harder to understand, and/or change the declaration in a way which is not meaningful to the user. A proper language support for template constraints is upcoming in the form of the Concepts Lite Technical Specification.
The Requires
type in this section corresponds to EnableIf
in the library. The Requires
type in the library takes a template-pack of conditions, combines them with logical-and, and passes that to EnableIf
.
The C++ Standard library provides the std::enable_if_t
template, for which std::enable_if_t<Condition, Type>
is equivalent to RequiresC<Condition, Type>
, and std::enable_if_t<Condition>
is equivalent to RequiresC<Condition, void>
. Pastel provides and uses Requires
, RequiresC
, DisableIf
, and DisableIfC
.