SFINAE

C++
Author

Quasar

Published

December 7, 2024

Introduction

When we write templates, we sometimes need to restrict the template arguments. For instance, we have a function that should work for any numeric type, therefore integral and floating point, but should not work with anything else. Or we may have a class template that should only accept trivial types for an argument.

There are also cases where we have overloaded function templates that should work with some types only. For instance, one overload should work with integral types and the other for floating-point types only. There are different ways to achieve that goal.

Type traits are, however, involved in one way or the other. The first one that will be discussed in this chapter is called SFINAE. C++20 concepts are an approach superior to SFINAE, that I am going to blog about in another post.

SFINAE stands for Substitution Failure Is Not An Error. When the compiler encounters the use of a function template, it substitutes the arguments in order to instantiate the template. If an error occurs at this point, it is not regarded as ill-informed code, only as a deduction failure. The function is removed from the overload set instead of causing an error. Only if there is no match in the overload set does an error occur.

An example of implementing the begin() method

In C++11, there are free-standing functions std::begin() and std::end() that return iterators to the first and the one-past-last elements of the container. These functions also work with arrays. How might we implement begin() to work both with STL containers and arrays?

We need two overloads of the function template:

#include <array>
#include <iterator>
#include <iostream>

template<typename T>
auto beginIter(T& c) { return c.begin(); }      //[1]

template<typename T, std::size_t N>
T* beginIter(T(&arr)[N]){ return arr; }         //[2]

The first overload calls the member function begin() and returns the value. Therefore, this overload is restricted to types that have a member function begin(), otherwise a compiler error would occur. The second overload simply returns a pointer to the first element of the array. This is restricted to array types; anything else would produce a compiler error.

We can use these overloads as follows:

int main()
{
    std::array<int, 5> arr1{1, 2, 3, 4, 5};     
    std::cout << *beginIter(arr1) << "\n";          //[3] prints 1

    int arr2[] {5, 4, 3, 2, 1};
    std::cout << *beginIter(arr2) << "\n";          //[5] prints 5
}

Compiler Explorer

If you compile this piece of code, no error, not even a warning occurs! The reason for that is SFINAE. When resolving the call to beginIter(arr1), substituting std::array<int,5> to the first overload at [1] succeeds, but the substitution for the second (at [2]) fails. Instead of issuing an error at this point, the compiler just ignores it, so it builds an overload set with a single instantiation, and therefore it can find a match for the invocation. Similarly, when resolving the call to beginIter(arr2), the substitution of int[5] for the first overload fails and is ignored, but it succeeds for the second and is added to the overload set, eventually finding a good match for the invocation. Therefore, both calls can be successfully made. Should one of the two overloads not be present, either beginIter(arr1) or beginIter(arr2) would fail to match the function template and a compiler error would occur.

Enabling SFINAE with the enable_if type trait

There are two categories of type traits in C++:

  • Type traits that enable us to query properties of the type at compile-time.
  • Type traits that enable us to perform type transformations at compile-time(such as adding or removing the const qualifier, or adding or removing pointer or reference from a type). These type traits are also called meta-functions.

One important type trait is std::enable_if. This is used to enable SFINAE and remove candidates from a function’s overload set. Recall that, enable_if<B,T> is a type metafunction. If B is true, it returns T.

template<bool B, typename T=void>
struct enable_if{};

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

Recall, the example in my blog post on type traits on the creating a serializer that exposes a uniform API to prirint an object to the output stream. To achieve that, we coded up a uses_write type trait.

With std::enable_if, we can implement that idea in a simple way:

template<typename T,
            typename std::enable_if<
                uses_write<T>::value>::type* = nullptr>

void serialize(std::ostream& os, T const & value){
    value.write(os);
}

template<typename T,
            typename std::enable_if<
                !uses_write<T>::value>::type* = nullptr>

void serialize(std::ostream& os, T const & value){
    os << value;
}

There are two overloaded function templates in this implementation. They both have two template parameters. The first parameter is the usual template type parameter T. The second is an anonymous non-type template parameter of a pointer type that also has the default value nullptr. We use std::enable_if to define the member called type only if the uses_write metafunction evaluates to true. Therefore, for classes that have the member function write, the substitution succeeds for the first overload but fails for the second overload, because typename* = nullptr is not a valid parameter. For classses for which the output stream operator << is overload, we have the opposite situation.

The std::enable_if metafunction can be used in several scenarios:

  • To define a template parameter that has a default argument.
  • To define a function parameter that has a default argument.
  • To specify the return type of a function.

Let’s use std::enable_if to define a function parameter with a default argument. For instance, we can write:

template<typename T>
void serialize(
    std::ostream& os, 
    T const & value,
    typename std::enable_if<use_write<T>::value>::type* value == nullptr
){
    value.write(os);
}

template<typename T>
void serialize(
    std::ostream& os, 
    T const & value,
    typename std::enable_if<!use_write<T>::value>::type* value == nullptr
){
    os << value;
}

We basically moved the parameter from the template parameter list to the function parameter list. The third alternative is to use std::enable_if<T> to wrap the return type of the function. This implementation is only slightly different(the default argument does not make sense for a return type.) Here is how it looks:

template<typename T>
typename std::enable_if<use_write<T>::value>::type serialize(
    std::ostream& os,
    T const & value
){
    value.write(os);
}

template<typename T>
typename std::enable_if<!use_write<T>::value>::type serialize(
    std::ostream& os,
    T const & value
){
    os << value;
}

In all these examples, the enable_if type trait was used to enable SFINAE during the overload resolution for the function templates. This type metafunction can also be used to restrict instantiations of class templates. In the following example, we have a class called integral_wrapper that is supposed to be instantiated only with integral types, and a class called floating_wrapper that is supposed to be instantiated only with only with floating point types:

#include <type_traits>

template<
    typename T,
    typename=std::enable_if<std::is_integral_v<T>>::type>
struct integral_wrapper{
    T value;
};

template<
    typename T,
    typename=std::enable_if<std::is_floating_point_v<T>>::type>
struct floating_point_wrapper{
    T value;
};

Both these templates have two type template parameters. The first one is called T, but the second one is anonymous and has a default argument. The value of this argument is defined or not with the help of the std::enable_if<B,T> type metafunction, based on the value of a boolean expression.

We can use the wrapper class templates as follows:

int main()
{
    integral_wrapper w1{ 42 };          //OK
    //integral_wrapper w2{ 42.0 };      //error
    //integral_wrapper w3{ "42" };      //error

    //floating_point_wrapper w4{ 42 };  //error
    floating_point_wrapper w5{ 42.0 };  //OK
    //floating_point_wrapper w6{ "42" };//error
    return 0;
}

Compiler Explorer

C++17 constexpr if

The C++17 feature if constexpr is a compile-time version of the if statement and makes SFINAE much easier. It helps replace complex template code with simpler versions. Let’s look at a C++17 implementation of the serialize function that can uniformly serialize both widgets and gadgets:

template<typename T>
void serialize(std::ostream& os, T const & value){
    if constexpr (uses_write<T>::value){
        value.write(os);
    }else{
        os << value;
    }
}

constexpr if enables us to discard a branch, at compile-time, based on the value of the expression. In our example, when the uses_write_v variable is true, the else branch is discarded, and the body of the first branch is retained. Otherwise, the opposite occurs. We end up with following specializations for the widget and gadget classes:

template<>
void serialize<widget>(std::ostream&& os, widget const & value){
    if constexpr(true)
    {
        value.write(os);
    }
}

void serialize<widget>(std::ostream&& os, widget const & value){
    if constexpr(false)
    {
        os << write;
    }
}