Substitution Failure is Not an Error – SFINAE

Substitution is part of the instantiation process, which responsible for the replacement of the template parameters with the deduced types. Failure at this stage will not produce a compilation error.

Previous article in series: Become a Compile-Time Coder
Next article in series: Template Base Class Collection

What is SFINAE?

“Substitution failure is not an error” means that if there is an error during the process of replacing the template parameters with the deduced/explicit specified ones, during the instantiation process, it won’t produce a compile-time error. Instead, it’ll remove the specialization from the overload set and keep looking for the next matching overload.

We saw it many times before in previous articles on this series, when we tried to apply restrictions on template params:

namespace std {
    string to_string(string &&val) {
        return val;
    }
}

template <typename T>
concept Stringable = requires(T type) {
    { std::to_string(type) };
};

template <Stringable ...Args>
std::string chain_strings(Args ...args) {
    return (std::string() + ... + std::to_string(args));
}

int main() {
    std::cout << chain_strings(2, " + ", 3.5, " = ", 2 + 3.5) << std::endl;

    return EXIT_SUCCESS;
}

At line 12 we set a restriction which force every argument that being sent to our function to be a Stringable argument. If one of the arguments is not Stringable, the specialization won’t be created and the compiler will look forward for another available overload. In our case, it won’t find any other available overload and a compile-time error will occur. We can change it by creating another overload to this function:

template <typename ...Args>
std::string chain_strings(Args ...args) {
    static_assert((Stringable<Args> && ...), "Error: can chain some of those params");
}

Now whenever we call our function with non-Stringable params, this overload will be called. We will discuss about the reasons to do/not do this kind of overload in this article.

When is it SFINAE?

SFINAE occur on type substitution errors in:

  • All expressions used in function signature – Return type, parameters.
template <int I>
void div(char(*)[I % 2 == 0] = 0) {
    // this overload is selected when I is even
}

template <int I>
void div(char(*)[I % 2 == 1] = 0) {
    // this overload is selected when I is odd
}

In the above example, for every specified I there will be always an overload that would generate an array with length of 0 which should generate a compile-time error in any other place in the code. However when used as part of expression with a template parameter as function param it will be a substitution failure.

template <typename ...Args>
typename std::enable_if_t<(std::is_arithmetic_v<Args> && ...), decltype((Args() + ...))> sum(Args ...args) {
    return (args + ...);
}

Here we can see that as the return type we have an expression, that uses std::enable_if_t to validate that the arguments which sent to the function are arithmetic. This expression will produce an error when one of the arguments won’t be an arithmetic, because we are trying to access the member type of std::enable_if that exists only if the given expression is true (for more details see a previous article on this series: The Exact Solution for a Generic Problem – Part 1).

However, if there is another overload for the function that will match our call for non-arithmetic arguments, this case won’t produce an error. It’s an expression that use template params on the return type section of the function, which makes it SFINAE case.

  • All expressions used in a template parameter declaration.
class base { public: virtual void func() = 0; };
class derived : public base { public: void func() override {} };
class goblin {};

template <typename T, typename = std::enable_if_t<std::is_base_of_v<base, T>>>
void call_func(T &&derived) {
    derived.func();
}

int main() {
    call_func(derived());
    // call_func(goblin()); // Compile-time error - candidate template ignored: requirement 'std::is_base_of_v<base, goblin>' was not satisfied [with T = goblin]
    return EXIT_SUCCESS;
}

Here we make sure that the deduced type T is a derived from base.

  • Since C++20: All expressions used in the explicit specifier.

In C++20 one of the features was a conditionally explicit specifier. This means that instead of creating an explicit function and non-explicit overload using SFINAE, we can use SFINAE as an explicit expression and gain the same result. It looks something like this:

struct my_goblin {
    my_goblin() {}
    my_goblin(int) {} // Enable conversion from int to my_goblin
};

template <typename T>
struct my_goblin_wrapper {
    explicit(!std::is_same_v<my_goblin, T>)
    my_goblin_wrapper(T &&type) : base (type) {}
    
    T &base;
};

int main() {
    my_goblin_wrapper mgw = my_goblin();
    // my_goblin_wrapper mgw1 = int(); // Without the explicit this line will compile.

    return EXIT_SUCCESS;
}

When to Use SFINAE and when Not?

It’s a good question (I’m totally objective, you can even ask me) and you deserve a good answer too.

We need to use SFINAE whenever there is a point to overload functions/classes with other type cases, when it’s logically reasonable. Assume we have a function named sum that accepts two unrelated params and returns their operator+ result:

template <typename T, typename U>
auto sum(T num1, U num2) {
    return num1 + num2;
}

You want to restrict the template parameters to be arithmetic. You might want to enable other types to be sent to this function, however you also might not. In case you want to close this function to this implementation, you have to not allow SFINAE in order to make sure that no-one will do so. Here comes to help static_assert that will produce an immediate error for non-arithmetic types that will be sent to this function:

// The only allowed sum function with two arguments
template <typename T, typename U>
auto my_sum(T num1, U num2) {
    static_assert(std::is_arithmetic_v<T> && std::is_arithmetic_v<U>, "Arguments have to be of arithmetic types.");

    return num1 + num2;
}

// Someone will try to walk around
struct my_int {
    explicit my_int(int a) : val(a) {}
    operator int() const { return val; }

    int val;
};

template <typename T, typename U, typename = std::enable_if_t<std::is_same_v<T, my_int>>>
auto my_sum(T num1, U num2) {
    return num1 + num2;
}

// ... main ...
auto res = my_sum(1, 2.3);
// auto res1 = my_sum(my_int(3), 2.3); // OOPS. An ambitious call. Remove the first method and the above call won't compile.

Conclusion

As you can see, SFINAE is here to help, but we might not want this help all the time. Always use the maximum restriction you can without functionality loss.

For more SFINAE details and examples see: cppreference – SFINAE

Full examples repository: cppsenioreas-metaprogramming-restrictions

More posts in this series:

2 thoughts on “Substitution Failure is Not an Error – SFINAE

Leave a comment