SFINAE constraints

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.

Context

The SFINAE rule is only in effect when it occurs in a template declaration. That is,

In particular,

Constraining the overload set

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.

Where to place the SFINAE-constraint

An ideal template-constraint-description

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.

SFINAE-constraint in a return-type

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.

SFINAE-constraint in a trailing 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.

SFINAE-constraint in a trailing argument-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.

SFINAE-constraint in a template-parameter type (C++11)

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.

SFINAE-constraint in a template-parameter value (C++11)

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);

SFINAE-constraint in a template-parameter value-pack (C++11)

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.

Summary

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.

Future

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.

Pastel

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.

Standard library

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.

Files

SFINAE machinery

Testing for SFINAE