As C++ coder, sooner or later you will encounter elements of meta-programming. It might be as simple a STL container. Or it might be a full blown template class that takes variadic arguments. I, myself, have also taken this journey. As I ventured deeper into meta-programming, acronyms such as CRTP, SFINAE really piqued my interest. Today, let’s talk about SFINAE.

SFINAE stands for “substitution failure is not an error”, and there has been numerous articles, stack overflow questions and blog posts on this topic:

My goal here is not to regurgitate what these excellent resources provide, but to summarize it in a concise way for my own learning and interpretation.

What problem does it solve?

SFINAE is an idiom, or a programming pattern in C++. At its core, it enables compile time introspection of template parameters for more compact and generalizable code that can be reused later. They are frequently associated template meta-programming - so you should think about when they might come in handy if you work with a lot of templated code.

Through SFINAE, we can write methods / struct / meta-helpers that help us answer questions like:

  • Does the template argument T passed into this class at compile-time implement the method foo()?
  • I want to have two template<int N> Array implementation (instead of having two different classes SmallArray, LargeArray) depending on the size of N.
  • I want to have my template function template<typename T> foo(T val) behave differently depending on some condition on T, such as if it’s a float type or an int type.

I also think of SFINAE pattern like function overload lookup on steroids, but also extended and applicable for classes, methods etc.

Prior to C++11, SFINAE implementations mostly uses template overload rules and clever use of the side-effects sizeof and typedefs to achieve this goal. With C++11’s introduction of std::decltype, std::declval, and std::enable_if, the SFINAE pattern becomes a little easier to read and understand.

Case Study

Source: Stack Overflow

C++11 switching f(T) based on whether T is an integer or a float

template<typename T>
std::enable_if_t<std::is_integral<T>::value> f(T t){
    //integral version
}

template<typename T>
std::enable_if_t<std::is_floating_point<T>::value> f(T t){
    //floating point version
}

std::enable_if_t will define a ::value of type void if the substitution is successful, otherwise, it will be undefined, and thereby the function declaration will be disabled at compile time.

To define a different return value type, we can supply our own type as the 2nd argument to std::enable_if_t, for example: std::enable_if_t<std::is_integral<T>::value, T> would evaluate to T only if the std::is_integral<T> is true.

Evaluate for logical conditions in the template parameter

template<int I> void div(char(*)[I % 2 == 0] = 0) {
    /* this is taken when I is even */
}

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

In this example, we have a special syntax [] enclosing some boolean expression after the function argument declaration in parenthesis. This was a pretty obscure feature in C++ called template argument deduction. This is another compile-time evaluation feature where [I % 2 ==0] is evaluated at compile time, and depending on what the expression evaluate to, will pick the either the even version, or the odd version.

As an aside, a pretty cool use-case of this template deduction is to extract the # of elements in an array:

template<typename T, std::size_t N>                 
constexpr std::size_t arraySize(T (&)[N]) noexcept  
{                                                   
  return N;                                         
}                                                   

Check whether a class has a certain member of certain type

// Header
template<typename T>
struct TypeSink{
    using Type = void;
};

template<typename T>
using TypeSinkT = typename TypeSink<T>::Type;

//use case
template<typename T, typename=void>
struct HasBarOfTypeInt : std::false_type{};

template<typename T>
struct HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>> :
    std::is_same<typename std::decay<decltype(std::declval<T&>().*(&T::bar))>::type,int>{};

struct S{
   int bar;
};
struct K{
};

template<typename T, typename = TypeSinkT<decltype(&T::bar)>>
void print(T){
    std::cout << "has bar" << std::endl;
}
void print(...){
    std::cout << "no bar" << std::endl;
}

int main(){
    print(S{});
    print(K{});
    std::cout << "bar is int: " << HasBarOfTypeInt<S>::value << std::endl;
}

This is a more complicated example, let’s work through it step by step.

First, we have a dummy struct TypeSink, of which, the Type in its scope is defined as a void type. The TypeSinkT is a type alias to refer to the underlying type in the TypeSink<T>::Type (I tried to remove it, and the compilers barfed).

HasBarOfTypeInt is another dummy struct that derives from std::false_type{} - which means HasBarOfTypeInt::value will evaluate to false. The second template argument typename=void in the struct means that the 2nd type argument will default to void type if unspecified.

In the template partial specialization of HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>> - the 2nd type argument is checking whether T has the member bar inside. If it does, the TypeSink<int>::Type is the final type. If T does not contain bar, then the whole expression will fail- triggering SFINAE (substitution failure is not an error) and results in the 2nd template argument evaluating to void, this then triggers the the fallback instantiation of std::false_type. We also note that this template partial specialization also derives from something else: std::is_same, is_same as explained in the official C++ reference, states that:

If T and U name the same type (taking into account const/volatile qualifications), provides the member constant value equal to true. Otherwise value is false.

Therefore, std::is_same is checking whether the declared type of bar is of type int. (std::decay removes const).

In the test code, we have partial template specialization of print() function that takes the 2nd argument of typename = TypeSinkT<decltype(&T::bar)>. This specialized function will get called when the typename = evaluates to some concrete type.

Check whether a template argument (non-Type) satisfies some criteria

Source: Stack Overflow

template<bool> struct Range;
template<typename T, int N = 0, typename = Range<true>>
class Channel
{
    // Implementation for all other cases of N
    // also, N defaults to 0
}

template<typename T, int N>
class Channel<T, N, Range<(N <= 0)> >
{
    // specialized implementation for when the N <= 0
    // also the default case since N defaults to 0
}

In this example, we define a dummy templated struct Range that has a single Boolean template argument. Depending on the value of the boolean, the Range<true> and Range<false> are considered two different types by the compiler. It appears that if N is known at compile time (either a const, static, or a constexpr), then the Range<(boolean expression)> can be evaluated at compile time. The final evaluated value of Range<> then triggers compiler template specialization lookup rules. At the end, the correct class is used.

Common Themes and Summary

  • Template specialization and function lookup rules are what enables SFINAE. i.e. C++ compilers will look for the most “specific” definition of a function or a class to use first, if it is not found, then a more general version is used instead.
  • Substitution failures is not an error - most SFINAE idioms rely on some lookup of an internal type, or some logic condition to evaluate to false - triggering a type system switch to point to some undefined type. In other words, we are intentionally triggering a failure in overload deduction - and causing the compiler to switch to the more general implementation instead
  • The syntax is really confusing - for example, decltype(std::declval<T&>().*(&T::bar)) refers to the declared type of the member variable bar of class T, but it is getting better with later C++ standards and better meta-programming support.
  • Avoid it at all costs if you do not need it. For most cases, function overloading and basic template specialization will do. You will rarely encounter application code where meta-programming on steroids is the only solution to the problem at hand.