Template SFINAE & type-traits

(869 words)

Today’s post is about template SFINAE & type-traits - cool C++ features with great compile-time power.

Template SFINAE

SFINAE is a scary-looking C++ acronym, which joins a long list of hard-to-remember capital-letter concepts (such as RAII, RVO, RTTI, PIMPL, etc).

SFINAE stands for “Substitution Failure In Not An Error”. Simply put, it means that when a compiler fails to substitute a template parameter it should continue looking instead of giving up.

Here’s a quick example:

#include <iostream>
using namespace std;

template <typename T> void foo(typename T::type) { cout << "1st" << endl; }
template <typename T> void foo(T) { cout << "2nd" << endl; }

struct MyStruct {
    using type = int;
};

int main() {
    foo<MyStruct>(2);  // ok - calls first version
    foo<int>(2);       // also ok - calls second version
}

Output:

1st
2nd

One important thing to note is that SFINAE, like its name suggests, only happens during substitution. Essentially it means that, like above, providing an error during function matching (aka overload resolution) is OK and the compiler will continue on its journey to find the proper function. However, failure inside a function’s body will yield an unrecoverable compiler error, afterwhich the compiler will not continue to search for other functions.

Here’s an example. If you don’t know what forward references are, you can safely ignore the second version of zoo(); All you need to know is that both functions could match on any parameter passed.

template <typename Container>
void zoo(const Container& container) {  // 1st version
    auto it = container.begin();
}

template <typename NonContainer>
void zoo(NonContainer&& non_container) {}  // 2nd version

int main() {
    std::less<int> l;

    // take a const-reference to ensure 1st version is called; If we used 'l'
    // the 2nd version would be preferred with NonContainer == std::less<int>&
    const std::less<int>& r = l;

    zoo(r);  // ERROR: no member named 'begin' in 'std::less<int>';
             // Compiler will *not* attempt to use the 2nd version of zoo
}

Type Traits

SFINAE has already been in C++98. C++11 introduces a new feature, not directly related but often used together: template-partial-specialization (which might have its own post at a later point). By combining these 2 features together one can create some powerful tricks.

Here’s a simple implementation of std::enable_if (found in <type_traits>):

// this is an actual complete implementation of std::enable_if found in std
// header <type_traits>
template <bool Condition, typename T = void>
struct enable_if {
    // No 'type' here, so any attempt to use it will fail substitution
};

// partial specialization for when Condition==true
template <typename T>
struct enable_if<true, T> {
    using type = T;
};

This simple-but-powerful struct allows us to set compile-time conditions on our functions and classes. The way one uses enable_if is by specifying a condition as the first template-parameter, and a type T (optional) that will be used if the condition is true. Take a minute to think about it as it’s not trivial.

Here’s a simple example of a function bar() which only accepts arguments that are enums. Attempting to pass non-enum will fail compilation:

// T must be an enum type.
// Second template argument is only used to enforce T's type, therefore it
// doesn't have a name and it is not used.
template <typename T,
          typename = typename enable_if<std::is_enum<T>::value, void>::type>
void bar(T t) {}

enum Enum1 { A, B };
enum class Enum2 { C, D };

int main() {
    bar(A);
    bar(Enum2::C);
    bar(1); // compile error - "no matching function for call to 'bar(int)'"
}

Another way to achieve basically the same thing is as follows:

// If T is enum - return value is void, otherwise - substitution failure.
template <typename T>
typename std::enable_if<std::is_enum<T>::value, void>::type bar2(T t) {}

// Rest unchanged.

Here’s a summary of the differences between bar() and bar2():

bar() bar2()
2 template arguments 1 template argument
Simple void return value Conditional, harder to understand return-value
Bypassable “security” (by specifying 2nd template argument) Unbreakable
Requires additional template parameter No additional parameters

Please note that the above bar*()s could have been implemented using static_assert() instead of enable_if. Choose the right tool for the job. Here’s a quick comparison:

enable_if static_assert()
Allows complicated overload rules without template specialization Forces compilation to fail when a criteria was not met
Takes place during function matching Only takes place after overload resolution, when a function has been selected
Long, harder to parse compilation errors Compile error is user-defined (2nd parameter to static_assert())

For more awesome type traits check out the standard C++ header <type_traits> - it’s full of goodies you might find useful. Some traits are even impossible to implement in C++, and require special compiler support (like has_virtual_destructor).

Try it out!

5 Minute Practice (use cpp.sh if you don’t have a compiler handy - it’s an online C++ compiler)

As a practice, try to implement the following classes (they all should define static constexpr bool value as true or false, according to their names:


Comments