Fadli Arsani
↑↓ to navigate · Enter to open · Esc to close

SFINAE tldr

The problem

Consider a class template with two overloads:

template<typename T>
class MyClass {
public:
    void f(T const& x);
    void f(T&& x);
};

MyClass<int> works fine. MyClass<int&> fails to compile:

error: 'void MyClass<T>::f(T&&)' cannot be overloaded with 'void MyClass<T>::f(const T&)'

The reason is reference collapsing. When T = int&:

  • T const& becomes int& const&, which collapses to int& (const on a reference is ignored because references cannot be cv-qualified, then & & collapses to &)
  • T&& becomes int& &&, which collapses to int& (& && collapses to &)

Both overloads produce void f(int&), an identical signature, so this is a hard error.

SFINAE

SFINAE is a technique where programmers leverage a C++ language rule to conditionally remove overloads at compile time. It stands for Substitution Failure Is Not An Error. Basically the idea is when the compiler substitutes template arguments into a function template and the resulting signature is invalid, it does not emit a hard error. It silently removes that overload from the candidate set and moves on.

In the problem above, SFINAE won’t really help. Why? Because SFINAE only applies during function template argument deduction (and partial specialization matching). The keyword to remember here is function template. In the example above, f is not a function template. It is a regular member function of a class template. When MyClass<int&> is instantiated, the compiler generates both f signatures as concrete functions, finds a duplicate, and errors, so no SFINAE.

The pre-C++20 fix (SFINAE)

So, to actually apply SFINAE here and fix our problem we make f a function template so that SFINAE can actually apply, then use std::enable_if to trigger a substitution failure when T is a reference:

template<typename T>
class MyClass {
public:
    void f(T const& x);

    template<typename U = T,
             typename = std::enable_if_t<!std::is_reference_v<U>>>
    void f(T&& x);
};

enable_if_t<false> is ill-formed (std::enable_if<false> has no ::type member, see llvm implementation of it for more details). Since this happens in an immediate context of template argument deduction (it is a default template parameter of a function template), that is a substitution failure, and SFINAE quietly removes the overload.

The modern fix (C++20 constraints)

In C++20, a requires clause replaces the enable_if machinery. Note that this is not considered as leveraging the SFINAE mechanics, because there’s really no substitution failure occurs. The compiler evaluates the constraint, and if it is false, the overload is excluded from the candidate set. In other words, in this case we have a different mechanism, but the same outcome.

template<typename T>
class MyClass {
public:
    void f(T const& x);

    void f(T&& x) requires (!std::is_reference_v<T>);
};

This works because a non-template member function of a class template is still a templated function per the standard, and templated functions can be constrained.

References