Tutorial: Let's get comfortable with SFINAE

27 January 2024

balloons and confetti for 1000 subscribers

SFINAE is a powerful tool in the C++ toolbox that allows us to select behavior based on “traits” of types. It relies on a peculiar behavior of C++, where errors during template type substitution are not fatal as long as there’s a substitution that succeeds. This is a very confusing concept, so I decided to make a fairly detailed video tutorial about it. In the video, I promised that if I get 1000 subscribers on YouTube, I would write a blog post about it so you may have access to the code snippets. So, here it is!

Today we will get comfortable with SFINAE in C++. SFINAE stands for “Substitution Failure Is Not An Error”. It’s an “accidental” feature of C++ and is up to C++20, the only way to do many of the things we will review in this post. Its syntax is often not very intuitive and since it relies on “inducing errors” to work, it can be rather confusing to understand.
In this post, we will go through a good number of examples so that by the end of it, you will be comfortable with SFINAE, be able to read SFINAE code and even write your own.

Recap: Templates

Before we dive into SFINAE, let’s remind ourselves what templates are and how they work.
A template is a “blueprint” for a function or a class. It’s not a function or a class itself, but rather a “description” of how to create one. The compiler will substitute the template types, with the types you use in your code. This is the substitution that SFINAE is all about. The compiler creates a new definition for each type.

template<typename T>
T foo(T t) { /* ... */}

foo<int>(42);
foo<double>(3.14);

For example, if you have the foo template function and you instantiate it with an int and double, the compiler will create two definitions. One for a foo that returns and int and takes an int argument, and another foo for the double.

What is SFINAE?

So, what is SFINAE? It stands for “Substitution Failure Is Not An Error”. The simplified idea is that the compiler will not generate an error when it tries to substitute the template types with the types you used in your code. This, as long as there’s some alternative way, or “candidate”, to replace them that will work.
In other words, if there are many candidates for an implementation, it’s OK if they fail to compile, during type substitution, as long as there is one candidate that succeeds.
A practical application of this is if you have two different implementations that check for equality between two numbers. If the arguments are floating point numbers you use one implementation, and if they are integers you use another.

template<typename T>
bool areEqual(T a, T b);

areEqual(1, 2); // When compering integrals use `==`
areEqual(1.0, 2.0); // When comparing floating points use something else

Why use SFINAE?

The goal of SFINAE is to select one implementation over the alternatives, based on some characteristic or “trait” of the types used in the template. For example, imagine a Player template class that gets an Addon type as a template parameter. So we have a Jetpack add-on, a Wings add-on, a Horse, a Boat and so on. We can create Players with any of these add-ons.

Player<Jetpack> playerThatCanFly1{};
Player<Wings> playerThatCanFly2{};
Player<Horse> playerThatCanRun{};
Player<Boat> playerThatCanSail{};

playerThatCanFly1.goToTarget(); // Fly to target avoiding tall buildings
playerThatCanFly2.goToTarget(); // Fly to target avoiding tall buildings
playerThatCanRun.goToTarget();  // Use roads to get to target
playerThatCanSail.goToTarget(); // Use waterways to get to target

With SFINAE, we can select a different implementation for the goToTarget member function of Player, when a Player has an add-on that provides flying capabilities.
I am not talking about any particular type. It’s not about the Jetpack or the Wings. It’s about the fact that an add-on provides the ability to fly. We do this for the other types too, the Horse, the Boat or anything else. The idea is that depending on the add-on’s characteristics, the goToTarget member function of Player does something different. And remember, since we are discussing templates, this is determined during compilation.

Player<Jetpack> playerThatCanFly{};
Player<Boat> playerThatCanSail{};

playerThatCanFly.fly();
playerThatCanSail.sail();

playerThatCanSail.fly(); // 💥 Won't compile
playerThatCanFly.sail(); // 💥 Won't compile

With SFINAE we can also “disable” certain implementations due to the presence, or absence, of specific characteristics. For example, a Player instantiated with the Boat add-on, cannot have a fly member function.
By disabling this function because of the absence of an Addon type with flying capabilities, we prevent our users from calling it by mistake. Compile-time checks are the ultimate way to prevent misuse of an API, correct?

With SFINAE we can also do introspection of a class. What does that mean?

void travelByAir();
void travelByLand();

template<typename T> // If type T has a fly() member function
void travel();       // use travelByAir(), else travelByLand()

We can check for example if a class has a specific member function or not.
If type T has a member function called fly then our travel function eventually makes a call to the travelByAir implementation. If it doesn’t, then it calls travelByLand.
Now, I am going to give you some very important advice. Are you ready for it? Do NOT use SFINAE unless you really need it. If you can do the same thing differently, usually, that other way is simpler and doesn’t give you the weird compilation errors SFINAE does.
Let’s check out the alternatives.

Overloading and template specialization

void foo(int i) { std::cout << "int" << std::endl; }
void foo(double d) { std::cout << "double" << std::endl; }

Do you know exactly what types you are going to use? Don’t use SFINAE. In other words, can you select a behavior based on which type it is instead of what the type can do? Then usually you can go for overloading. Rather simple and easy to understand. Is overloading not enough and you need templates? Then specialize them.

template <typename T>
bool compare(T a, T b) { return a == b; }

template <>
bool compare<double>(double a, double b) {
    return std::abs(a - b) < 1e-6;
}

Don’t use SFINAE if the behavior you are going to choose between depends on specific types rather than characteristics.

Tag dispatching

Another alternative is to use tag dispatching. Unlike the two previous techniques, here we can select a behavior based on characteristics of a type. We combine templates and overloading as we select a behavior based on the tag type.

template<typename T>
auto get_value_impl(T t, std::true_type) { return t; }

template<typename T>
auto get_value_impl(T t, std::false_type) { std::cout << "not arithmetic\n"; }

template<typename T>
auto get_value(T t) { return get_value_impl(t, std::is_arithmetic<T>()); }

auto a = get_value(1);
auto b = get_value(4.2);
/* auto c = */ get_value("hello");

If we pass a number, we call the first overload of get_value_impl, otherwise the second. We often use tag dispatching for creating our own type traits to use with SFINAE. But regardless, if you can use tag dispatching instead of SFINAE, go for it.

Empty classes as tags

struct IntTag{};
struct FloatTag{};

template<typename T>
T square_impl(T value, IntTag) { /* Integer implementation */ }

template<typename T>
T square_impl(T value, FloatTag) { /* Floating point implementation */}

template<typename T>
T square(T value) {
    if (std::is_integral<T>::value) {
        return square_impl(value, IntTag{});
    }
    if (std::is_floating_point<T>::value) {
        return square_impl(value, FloatTag{});
    }
    std::cerr << "Unsupported type for square calculation." << std::endl;
    return T{};
}

A variant of tag dispatching you may see is to use classes as tags. This is effectively the same thing as before, but now we can have more than two tags. Instead of having just true_type and false_type, we can have a tag for every behavior we want to select.

Non-type template parameters as tags

template<typename T, int S>
struct ToStringImpl {
    static std::string toString(const T& x) { return std::to_string(x); }
};

template<typename T>
struct ToStringImpl<T, 0> {
    static std::string toString(T x) { return std::string{x}; }
};

template<typename T>
auto toString(T x) {
    constexpr int hasStdToString = std::is_arithmetic_v<T> ? 1 : 0;
    return ToStringImpl<T, hasStdToString>::toString(x);
}

// If `S` is `0` go for the 2nd implementation as the "most specialized"

C++ is notorious for allowing you to do the same thing in many different ways.
Yet another way to do tag dispatching is by using non-type template parameters as tags. This is a bit less readable, but slightly better than SFINAE.
We are partially specializing the ToStringImpl template class, based on the value of the non-type template parameter. The compiler will select “the most specialized” implementation. So when we call toString with type T not being a number, then ToStringImpl is instantiated with 0 as the second template parameter. As a result, the specialization is selected over the base case, as the most specialized one. If T is a number, then the base case is selected.

if constexpr (C++17)

As of C++17, we have if constexpr which is a compile-time if statement. This means that only the code that’s inside the branch that’s being followed gets compiled.

template<typename T>
bool areEqual(T a, T b) {
    if constexpr (std::is_floating_point_v<T>) {
        return std::abs(a - b) < 0.0001;
    } else {
        return a == b;
    }
}

If you can get away with using if constexpr instead of SFINAE, you should certainly do it. It’s way simpler and more readable.

static_assert for disabling functionality

template<typename T>
struct AddOrAppend {
    void add(T e) {
        static_assert(std::is_arithmetic_v<T>, "T can't be added");
    }

    void append(T e) {
        static_assert(!std::is_arithmetic_v<T>, "T can't be appended");
    }
};

AddOrAppend<int> a{};
a.add(2);
a.append(3); // Will not compile 💥
AddOrAppend<std::string> b{};
b.add("World"); // Will not compile 💥
b.append("World");

If you want to disable functionality based on a characteristic of a type, you could use static_assert instead of SFINAE. In this example, we would like to have a class that can either add or append elements. If the type T is arithmetic, then we can add elements, otherwise we can append them.

Concepts (C++20)

template<typename T>
    requires std::is_floating_point_v<T>
bool areEqual(T a, T b) {
    return std::abs(a - b) < 0.0001;
}

template<typename T>
bool areEqual(T a, T b) {
    return a == b;
}

If you are using C++20, you can use concepts. With concepts, choosing the right implementation is done in a more readable way than SFINAE. Additionally, the compiler errors you get are considerably better. In fact, once concepts become more widespread, I expect to see SFINAE less and less. Nonetheless, the SFINAE mindset is still useful to know and understand.
In this example, the first implementation is selected as “the most specialized one”, if type T is a floating point number. Overall, I suspect that everything you can do with SFINAE you can do with concepts but in a more readable way.

Don’t use SFINAE if there’s a better alternative

I know I wrote this before, but I want to repeat it. Use SFINAE only as a last resort. Go for overloads and template specialization if you are working with specific types. If you can get away with it, go for tag dispatching or if constexpr. And of course, concepts if you’re on a C++20 codebase. To summarize, here’s a list of alternatives to SFINAE:

  • Overloads
  • Template specialization
  • Tag dispatching
  • if constexpr (C++17)
  • static_assert
  • Concepts (C++20)

Back to SFINAE

Now that you know of some potential alternatives, let’s take a look at SFINAE. The goal of SFINAE is for the intended implementation to be selected over the alternatives which should be uncompileable.
With templates, we can exploit errors in:

  • The template parameter list
  • The template return type
  • The function arguments

Let’s start with the “simplest” case and work our way up. Take a look at the implementation of areEqual. The goal is the same as before: When we compare two floating point numbers, the equality operator is not good enough.

template<typename T, int I>
bool areEqualImpl(T a, T b, char (*f)[I % 2 == 0] = nullptr) { return a == b; }

template<typename T, int I>
bool areEqualImpl(T a, T b, char (*f)[I % 2 == 1] = nullptr) { return std::abs(a - b) < 0.1; }

template<typename T>
bool areEqual(T a, T b) {
    constexpr int isFloating = std::is_floating_point_v<T> ? 1 : 0;
    return areEqualImpl<T, isFloating>(a, b);
}

// ⚠️ C-style arrays of `0` or negative size are not allowed ⚠️
// Read `char (*f)[I % 2 == 0] = nullptr` as:
// `char` array named `f` of size `I % 2 == 0`, default value `nullptr`
// If `I % 2 == 0` is `true`, casted to `std::size_t` becomes `1`

We will take advantage of a rule that says that C-style arrays need to have a size larger than 0. Compiling an array of size 0 will fail.

Now, take a look at the second argument of the areEqualImpl functions. It looks weird but it’s not something exotic:

  • It is a char C-style array, named f.
  • Its size is I % 2 compared to 0 or 1.
  • It has the default value of nullptr.
  • We default it to nullptr because we don’t want to care about it. The only purpose with it is to induce a compilation error.

Next, let’s focus at the first areEqualImpl function. If I is an even number, then I % 2 is 0. When we compare it to 0 the result is true. When we use true as the size of an array, true casted to a number becomes 1. In other words, we end up with an array of size 1, something that’s perfectly legal.
Let’s take a look at the second areEqualImpl function. If I is an even number, then I % 2 is 0. When we compare it to 1 the result is false. false casted to a number becomes 0. An array of size 0 is illegal, so we get a compilation error. Is it reported?
No, because the compiler will not report a substitution error if there’s an alternative viable candidate. And what is the alternative viable candidate? The first areEqualImpl function.

When a floating point type is passed to areEqual

Let’s break things down even further. Take a look at what happens when we call areEqual with two floating point numbers. First, the isFloating will become 1. Then, areEqualImpl will be instantiated with 1 as the second template parameter.

template<typename T, int I>
bool areEqualImpl(T a, T b, char (*f)[0] = nullptr) // 🔴 Fails to compile
{                                                   // but not an error if
    return a == b;                                  // another implementation
}                                                   // is available

template<typename T, int I>
bool areEqualImpl(T a, T b, char (*f)[1] = nullptr) // 💚 Compiles since
{                                                   // `I % 2 == 1` is `true`
    return std::abs(a - b) < 0.01;                  // which casted to `size_t`
}                                                   // becomes `1`

bool areEqual(float a, float b) {
    constexpr int isFloating = 1; // 💡
    return areEqualImpl<float, isFloating>(a, b);
}

It’s an odd number, so what happens? The first areEqualImpl function fails to compile. Remember, the size of the array there is only valid for even numbers. However, the second one succeeds so it’s selected!

When a non-floating point is passed to areEqual

template<typename T, int I>
bool areEqualImpl(T a, T b, char (*f)[1] = nullptr) // 💚 Compiles since
{                                                   // `I % 2 == 0` is `true`
    return a == b;                                  // which casted to `size_t`
}                                                   // becomes `1`

template<typename T, int I>
bool areEqualImpl(T a, T b, char (*f)[0] = nullptr) // 🔴 Fails to compile
{                                                   // but not an error if
    return std::abs(a - b) < 0.01;                  // another implementation
}                                                   // is available

bool areEqual(int a, int b) {
    constexpr int isFloating = 0; // 💡
    return areEqualImpl<int, isFloating>(a, b);
}

What happens when T is not a floating point number?

  • isFloating becomes 0.
  • 0 is an even number. So as we described before, with an even number the first areEqualImpl function succeeds.
  • The second candidate gets an array of size 0 as its second argument, so it fails to compile.

The error is not reported because the first areEqualImpl function is a viable candidate. Confusing? I know.
I hope that by the end of this tutorial, you will have become fluent in this.

Different behavior based on property/capability

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Bullets)* = nullptr) {
    r.shootBullets();
}

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Missiles)* = nullptr) {
    r.shootMissiles();
}

Let’s try some introspection. Let’s check if a class has a static member variable called Bullets. If that’s the case we should pick the first implementation. If it has Missiles it should pick the second.
Like in the previous example, we take advantage of substitution failures at the function parameters. Particularly the second argument of the functions. If you find the decltype expression strange looking let me simplify it for you:
It means that the second argument is a pointer, of the same type as Robot::Bullets or Missiles. It does not have a name and its default value is nullptr. We use the same trick as before with the defaulted argument that we don’t want to care about when using the function, just to induce a compilation error.

struct RobotA {
    static const int Bullets{};
    void shootBullets() {}
};

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Bullets)* = nullptr) { // 💚 Compiles
    r.shootBullets();                            // since `RobotA::Bullets` exists
}

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Missiles)* = nullptr) { // 🔴 Fails
    r.shootMissiles();                  // to compile, since `RobotA::Missiles`
}                                       // doesn't exist

RobotA robotA{};
handleEnemies(robotA); // 🚧

If we use handleEnemies with RobotA that has Bullets as a static member variable, then the second implementation fails to compile since RobotA::Missiles does not exist. Is that a problem?
It’s not, as long as there’s an alternative viable candidate. And which one is that? The first implementation.

struct RobotB {
    static const int Missiles{};
    void shootMissiles() {}
};

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Bullets)* = nullptr) { // 🔴 Fails
    r.shootBullets();                  // to compile, since `RobotB::Bullets`
}                                      // doesn't exist

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Missiles)* = nullptr) { // 💚 Compiles
    r.shootMissiles();                           // since `RobotB::Missiles` exists
}

RobotB robotB{};
handleEnemies(robotB); // 🚧

Similarly, if we use handleEnemies with RobotB that has Missiles as a static member variable, then the first implementation fails to compile since RobotB::Bullets does not exist.

SFINAE on the return type

template<typename S>
typename S::IpV4 getAddress(S& sensor) {
    return sensor.ipV4;
}

template<typename S>
typename S::IpV6 getAddress(S& sensor) {
    return sensor.ipV6;
}

// 💡 `typename` before the return type, since it's a *dependent name*

Another way to use SFINAE is by inducing a failure on the return type. Here we would like to use the first implementation of getAddress if the type S has a subclass called IpV4. If it has a subclass called IpV6 we would like to use the second implementation.

struct SensorA {
    struct IpV4 { int address{}; };

    IpV4 ipV4{};
};

template<typename S>
typename S::IpV4 getAddress(S& sensor) { // 💚 Compiles
    return sensor.ipV4;
}

template<typename S>
typename S::IpV6 getAddress(S& sensor) { // 🔴 Fails to compile
    return sensor.ipV6;                  // since `SensorA::IpV6` doesn't exist
}

SensorA sensorA{};
getAddress(sensorA); // 💡

What happens if we instantiate getAddress with SensorA as its type?
Well, SensorA has a subclass called IpV4 subclass, so the first implementation is selected. The second implementation fails to compile because the SensorA::IpV6 subclass does not exist.
Similarly, if we were to pass a SensorB that has an IpV6 subclass, then the first implementation would fail to compile.

Before we move on, I’d like to ask you a question. There’s a SensorC that has both IpV4 and IpV6 subclasses. What happens then? Which implementation is selected?

struct SensorC {
    struct IpV4 { int address{}; };
    struct IpV6 { int address{}; };
    IpV4 ipV4{};
    IpV6 ipV6{};
};

template<typename S>
typename S::IpV4 getAddress(S& sensor) { return sensor.ipV4; }

template<typename S>
typename S::IpV6 getAddress(S& sensor) { return sensor.ipV6; }

This will not compile because getAddress is now ambiguous. You have two candidates that are equally suitable for the call, so the compiler doesn’t know which one to pick. So yes, SFINAE as long as there’s a candidate that compiles but there can’t be too many viable candidates either.

So far I have shown you how to use SFINAE without introducing any “new” or “exotic” features of the language. We talked about a C-style array of size 0 that cannot compile and a member variable or a subclass that does not exist. Let’s take a look at our, failure inducing, SFINAE superweapon: std::enable_if.

std::enable_if: A more flexible way to SFINAE

std::enable_if is a template class that has a public member type definition called type, equal to the second template parameter, if the first template parameter is true.
On the other hand, if the first template parameter is false, then there’s no type member type definition.

template<bool B, class T = void>
struct enable_if;

Let’s take a look at perhaps the simplest example. If the first template parameter is true, then std::enable_if has a public member type definition called type. The type of “type” is the second template parameter.
In the first line, we instantiate std::enable_if with true as the first template parameter and int as the second. So the std::enable_if “type” exists and it’s an integer. The first line is equivalent to writing int i is 0. We can do this with any type, a string will also work as we see in the second line.
If we do not specify the second template parameter, it defaults to void. Now, take a look at the last line. The first template parameter is false. What does this mean? It means there’s no type member type definition. Therefore the last line will not compile.

std::enable_if<true, int>::type i = 0;           // int i = 0;
std::enable_if<true, std::string>::type j{"hi"}; // std::string j{"hi"};
(std::enable_if<true>::type) i;                  // (void) i;

std::enable_if<false, int>::type z = 0; // Won't compile, no ::type 💥

std::enable_if a bit more realistically

Let’s take a look at some more realistic examples with std::enable_if, so you can get familiar with the syntax. The type float is a floating point number, so the first template parameter is true. As a result, std::enable_if has a public member type definition called type of type int.

using T = std::enable_if<std::is_floating_point_v<float>, int>::type;
std::cout << typeid(T).name() << std::endl;// i(nteger)
using U = std::enable_if<std::is_floating_point_v<float>>::type;
std::cout << typeid(U).name() << std::endl;// v(oid) by default

using Z = std::enable_if<std::is_integral_v<float>>::type; // No ::type 💥

If we don’t specify the second template parameter, it defaults to void, as we can see in the second example.
What happens if the first template parameter is false? Then there’s no type definition. In other words, the last line will not compile.
A float is not an integral number, so the first template parameter is false. Trying to access the type member type definition will fail during compilation.

std::enable_if_t

float a{1.0F};
using T = std::enable_if_t<std::is_floating_point_v<decltype(a)>, int>;
std::cout << typeid(T).name() << std::endl; // i(nteger)

using U = std::enable_if_t<std::is_floating_point_v<decltype(a)>>;
std::cout << typeid(U).name() << std::endl; // v(oid)

using Z = std::enable_if_t<std::is_integral_v<decltype(a)>, void>; // 💥

std::enable_if_t is a type alias for std::enable_if<...>::type. Use it to skip writing ::type after std::enable_if.

std::enable_if<false>

int a{1};
using T = std::enable_if<std::is_floating_point_v<decltype(a)>, int>::type;
std::cout << typeid(T).name() << std::endl;

One may ask, what’s the point of std::enable_if with false as the first template parameter? It doesn’t compile. Why would we ever need something that does not compile? The answer is that it becomes our SFINAE super-weapon.
Do you remember all the examples we saw before that were not using std::enable_if? It’s unlikely you will see them in real code. If you want to induce a failure, the most readable and flexible way to do it is with std::enable_if.

SFINAE with std::enable_if

Let’s take a look at the equality implementations again. Now we are using std::enable_if to induce a substitution failure on the third argument. When we call areEqual with two floating point numbers, the first implementation is selected and the second one fails to compile.

template<typename T>
bool areEqual(T a, T b, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr) {
    return std::abs(a - b) < 0.0001;
}

template<typename T>
bool areEqual(T a, T b, std::enable_if_t<!std::is_floating_point_v<T>>* = nullptr) {
    return a == b;
}

Now focus at the first candidate. When T is a float or a double the first template parameter of std::enable_if is true. As a result, the type member definition exists, so this compiles. Since a second template parameter is not provided, it defaults to void.
The second implementation has a negation in front of the floating point check. This means, it will fail to compile because the first template parameter of std::enable_if becomes false. As a result, there’s no type member.
Let’s try some substitutions, with T being a double:

bool areEqual(double a, double b, void* = nullptr) { // 💚 Compiles with an
    return std::abs(a - b) < 0.0001; // unnamed and defaulted void* argument
}

bool areEqual(double a, double b, std::enable_if_t<!true>* = nullptr) { // 🔴
    return a == b; // Fails to compile, since `std::enable_if_t<false>`
}                  // doesn't name a type

If T is a double, then the third argument, of the first implementation, is a void pointer. It is also unnamed and has a default value of null. This means we can ignore it when calling the function.
On the other hand, the second candidate fails to compile because, well, not true is false and therefore no type member exists.

SFINAE on the return type

You may also fail substitution on the return type. As a result, in this example, we don’t need to use a third argument just for the sake SFINAE:

template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, bool> areEqual(T a, T b) {
    return std::abs(a - b) < 0.0001;
}

template<typename T>
std::enable_if_t<!std::is_floating_point_v<T>, bool> areEqual(T a, T b) {
    return a == b;
}

We use std::enable_if with the intended return type, bool, as the second template parameter. If T is a floating point number, then only the first candidate will compile and the return type will be a bool. If T is not a floating point number, then only the second candidate will compile. If you haven’t seen std::enable_if before, you may find this piece of code not-so-readable. Don’t worry, I trust that by the end of this tutorial you’ll be able to read it like a pro.

SFINAE on the template parameter list

template<typename T, typename std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
bool areEqual(T a, T b) {
    return std::abs(a - b) < 0.0001;
}

template<typename T, typename std::enable_if_t<!std::is_floating_point_v<T>, int> = 0>
bool areEqual(T a, T b) {
    return a == b;
}

We haven’t seen this substitution failure before, but we can also use std::enable_if to induce SFINAE on the template parameter list. If substitution succeeds, the second type of the template parameter list will be of type int. It will be defaulted to 0 and unnamed, so we can ignore it.
The idea is that the substitution should only succeed on one of the candidates only. If T is a double or a float, then the first implementation will be selected, otherwise the second. Not so readable is it?

Looking at the previous example, one may ask, why do we need to create a non-type template parameter and default it? Can’t we instead use an unnamed type template parameter of type void or whatever? Something like this:

template<typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
bool areEqual(T a, T b) {
    return std::abs(a - b) < 0.0001;
}

template<typename T, typename = std::enable_if_t<!std::is_floating_point_v<T>>>
bool areEqual(T a, T b) {
    return a == b;
}

// 💥 redefinition of template<class T, class> bool areEqual(T, T)
// *Default template arguments* not considered in function template equivalency

Unfortunately, that won’t work. The reason is buried “deep” within the standard.
The problem with SFINAE is that it often forces you to look deep into the standard to troubleshoot your code. To make matters worse, the error messages are usually not very helpful either. So, is there a way to make things more readable? Stay with me to find out.

Making your own traits

I think we can all agree by now that SFINAE looks a bit ugly. Can we make it better? Yes, thankfully, we can by creating our own traits. Of course, creating our own traits is not only about making things more readable, but also about reusability.

Here’s a very simple trait. isFloat and isFloatAlt, which are pretty much equivalent.

template<typename T>
struct IsFloat : std::false_type {};

template<>
struct IsFloat<float> : std::true_type {};

template<typename T>
struct IsFloatAlt {
    static constexpr bool value = false;
};

template<>
struct IsFloatAlt<float> {
    static constexpr bool value = true;
};

isFloat is a template class that extends std::false_type in its base case. If T is a float, then the specialization is chosen and it extends std::true_type instead.
isFloatAlt does not extend anything but, instead, it has a static member variable called value. In its base case, the value is false. When the type is float, the specialization is chosen and the value is true.
This was a very simple and rather redundant trait, but you can get an idea of how to create your own traits. Let’s see how isFloat could be used.

template<typename T>
std::enable_if_t<                                       // Return type is
        IsFloat<T>::value || std::is_same_v<T, double>, // bool if T is float
        bool>                                           // or double
areEqual(T a, T b) {
    return std::abs(a - b) < 0.0001;
}

template<typename T>
std::enable_if_t<
        !IsFloat<T>::value && !std::is_same_v<T, double>,
        bool>
areEqual(T a, T b) {
    return a == b;
}

Again, this is a rather dumb example, but let’s go with it for the sake of simplicity. In the first candidate, the first template parameter of std::enable_if is true if isFloat::value is true or if the type is double. We haven’t achieved anything different or better than before, but I wanted to show you how to use traits. You will, often, have to create your own traits and trait combinations to achieve what you want.

Combination of traits

Here are some traits that… combine other traits. The first one, isFloatOrDouble combined the IsFloat and IsDouble traits. It does so with a disjunction which is the logical OR of template metaprogramming. IsNotFloatOrDouble is the negation of the previous trait and IsFloatAndTriviallyConstructible used conjunction which is the logical AND for a list of types.

The benefit of creating these custom traits is (1) readability and (2) you can reuse them in multiple places.

template<typename T>
struct IsFloatOrDouble : std::disjunction<IsFloat<T>, IsDouble<T>> {};

template<typename T>
struct IsNotFloatOrDouble : std::negation<IsFloatOrDouble<T>> {};

template<typename T>
struct IsFloatAndTriviallyConstructible
    : std::conjunction<IsFloat<T>, std::is_trivially_constructible<T>> {};

// std::disjunction is the logical OR of a list of traits
// std::negation is the logical NOT of a trait
// std::conjunction is the logical AND of a list of traits

I don’t want to make you upset but another common way to create a trait is… with SFINAE. Here, we have the IsIntegral base case that extends std::false_type. We’ve seen this before.

template<typename T, typename = void>
struct IsIntegral : std::false_type {};

template<typename T>
struct IsIntegral<T, std::enable_if_t<std::is_integral_v<T>>> : std::true_type {};

The new part is that we have a second template parameter, that is unnamed and defaults to void. Next, there’s a specialization that extends std::true_type. It will be selected if T is an integral number and the std::enable_if type compiles:

  1. Calling IsIntegral<T> with some T results to IsIntegral<T, void>
  2. The template parameter list is compared against specializations
  3. If no specialization matches, the base case is used as a fallback
  4. Compile error if T is not integral, but it’s OK since SFINAE
  5. If T is integral, the specialization becomes IsIntegral<T, void>
    • Selected over the base case, as the most specialized choice

One thing you need to watch out for is the type of the second template parameter. The types of the base case and the specialization must be the same. In the previous example, we used void which is the default type of std::enable_if.
If we use a different type, we need to make sure that if the substitution succeeds, the types match, otherwise the specialization will not be selected. It is only when we have matching specializations the “most specialized” one will be chosen:

template<typename T, typename = int> // 👈 this is `int` by default now
struct IsIntegral : std::false_type {};

template<typename T>
struct IsIntegral<T, std::enable_if_t<std::is_integral_v<T>>> : std::true_type {};

// ⚠️ The base case parameter list types **must** match the specialization's
  1. Calling IsIntegral<T> with some T results to IsIntegral<T, int>
  2. The template parameter list is compared against specializations
  3. If T is integral, the specialization becomes IsIntegral<T, void>
  4. The base case is chosen since IsIntegral<T, int> was needed
    • Partial specialization <T, void> doesn’t match <T, int>
    • Only when you have matching specializations the most specialized one is chosen

std::void_t

Another handful type we use with traits is std::void_t. What it does, is turn any type or list of types into void. I know what you are thinking. Why would that be useful? Well, it ties in with the previous example:
To consider a specialization, it needs to match the type of the base case.

template<typename T, typename = void>
struct HasBullets : std::false_type {};

template<typename T>
struct HasBullets<T, std::void_t<decltype(T::Bullets)>> : std::true_type {};

// `std::void_t` is a "kitchen sink" type that maps types to `void`

Let’s consider this HasBullets example which should inherit true_type if type T has a static member variable called Bullets.
If we skipped the std::void_t part, how would we match the types of the base case and the specialization? We don’t know if T::Bullets exists or not, so we can’t use it in the base case. Instead, we use void in the base case and wrap T::Bullets into std::void_t in the specialization. If T::Bullets exists, then void_t<T::Bullets> is void and the specialization will be selected. If T::Bullets does not exist, then SFINAE and the base case will be selected.

Looking at an earlier example, is there a way, for a robot without missiles or bullets, to handleEnemies?

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Bullets)* = nullptr) {
    r.shootBullets();
}

template<class Robot>
void handleEnemies(Robot& r, decltype(Robot::Missiles)* = nullptr) {
    r.shootMissiles();
}

concepts aside, you can do it with std::enable_if and a combination of traits.

template<typename T, typename = void>
struct HasBullets : std::false_type {};

template<typename T>
struct HasBullets<T, std::void_t<decltype(T::Bullets)>> : std::true_type {};

template<typename T, typename = void>
struct HasMissiles : std::false_type {};

template<typename T>
struct HasMissiles<T, std::void_t<decltype(T::Missiles)>> : std::true_type {};

template<typename T>
struct NoWeaponry : std::conjunction<std::negation<HasBullets<T>>,
                                     std::negation<HasMissiles<T>>> {};

As a first step, we create several traits to check if a robot has missiles, bullets or no weapons at all. Then we will use these traits to create another handleEnemies candidate.

template<typename Robot>
std::enable_if_t<HasBullets<Robot>::value> handleEnemies(Robot& r) {
    r.shootBullets();
}

template<typename Robot>
std::enable_if_t<HasMissiles<Robot>::value> handleEnemies(Robot& r) {
    r.shootMissiles();
}

template<typename Robot>
std::enable_if_t<NoWeaponry<Robot>::value> handleEnemies(Robot& r) {
    std::cout << "I surrender\n";
}

// ⏳ Remember: By default `std::enable_if_t` is `void` if condition `true`

This is one way to refactor the handleEnemies functions to use traits, SFINAE and handle the case where a robot has no weapons at all. We take advantage of SFINAE on the return type to select the right implementation. Remember, since we are not specifying a second template parameter for std::enable_if, the return type of the candidate that succeeds in compiling will be void.

Keep in mind that you can create your own aliases for the traits. Instead of typing ::value for example, you can create an alias called HasBullets_v.

template<typename T, typename = void>
struct HasBullets : std::false_type {};

template<typename T>
struct HasBullets<T, std::void_t<decltype(T::Bullets)>> : std::true_type {};

template<typename T>
constexpr bool HasBullets_v = HasBullets<T>::value;

Then you can do:

template<typename Robot>
std::enable_if_t<HasBullets_v<Robot>> handleEnemies(Robot& r) {
    r.shootBullets();
}

Different behavior based on functionality

Let’s get back to more cool things you can do with traits and SFINAE. Let’s learn how to create a trait that tells us whether a class has a member function or not.

template<typename Player, typename = void>
struct CanRun : std::false_type {};

template<typename Player>
struct CanRun<Player, std::void_t<decltype(std::declval<Player>().run())>>
    : std::true_type {};

Here we use std::declval which allows us to “create” a “fake” instance of a type for compile-time evaluation purposes. If the run() member function exists, then whatever its type is, it becomes void and the specialization is selected.
If Player does not have a run() member function, then SFINAE and we use the base case. This is another very powerful technique that allows us to do introspection of a class and expose it via a very useful and readable trait.
To summarize, we create a fake instance of Player for evaluation and call its run() member function. If that function exists, it has a type. We wrap that type into void so that it matches the base case. If the run() function does not exist, then SFINAE.

Let’s see how we’d use this trait to choose one implementation over another:

template<typename T, typename = void>
struct CanRun : std::false_type {};

template<typename T>
struct CanRun<T, std::void_t<decltype(std::declval<T>().run())>> : std::true_type {};

template<typename Player>
std::enable_if_t<CanRun<Player>::value> play(Player& p) { p.run(); }

template<typename Player>
std::enable_if_t<!CanRun<Player>::value> play(Player& p) { p.walk(); }

struct Runner { void run() { std::cout << "Run!\n"; }};
struct Walker { void walk() { std::cout << "Walk!\n"; }};

Runner r{};
play(r); // Run!
Walker w{};
play(w); // Walk!

There’s the play function that takes a Player and calls its run member function if it can run. We use the trait we previously created to do SFINAE on the return type.
Note that in this code we use SFINAE on two occasions. (1) on the return type of the different play candidates and (2) at the CanRun trait specialization. SFINAE on top of SFINAE, isn’t that beautiful?

Choose a different constructor with SFINAE

So far we have demonstrated how to select different behaviors with SFINAE, using free functions. You can do this for functions that are members of a class too. There’s only one catch.

template<typename T>
struct ConditionalCtor {
    template<typename U = T>
    ConditionalCtor(std::enable_if_t<std::is_integral<U>::value>* = nullptr) {
        std::cout << "Integral constructor" << std::endl;
    }

    template<typename U = T>
    ConditionalCtor(std::enable_if_t<!std::is_integral<U>::value>* = nullptr) {
        std::cout << "Non-integral constructor" << std::endl;
    }
};

// SFINAE on the default argument of the constructor
// You need `template<typename U = T>` to avoid "hijacking" `T`

Here we see how we can choose a constructor depending on whether type T is integral or not. We exploit SFINAE in the unnamed defaulted argument of the constructor. The only difference is that we must “redeclare” the class types we want to use, in the template parameter list of the function. You can see that on the line above the constructor declaration: template with type U being by default T.
When first encountering this, it was completely not obvious to me why this was necessary. And, to be honest, it still isn’t. I’ve read some explanations about this but nothing that really convinced me on why it is necessary.
Anyway, the idea is that you cannot use the class type T to do SFINAE in class members directly, because you are “hijacking” it. You need to create a new type U and use that instead. Could it have been done better? I think so. But that’s how it is.
What I want you to keep from this snippet is that you can use SFINAE in class members too. If the substitution depends on a class template type parameter, you need to use it with a different name.

Conditionally disable class member functions

So far, we select behavior “symmetrically”. If the type satisfies a characteristic, do this, otherwise do that. We always provided an alternative. What happens if we don’t? That’s totally fine and very useful in fact. We may want some features available only for certain traits. Take a look at this class for example:

template<typename T>
struct ConditionalMethod {
    template<typename U = T>
    std::enable_if_t<std::is_integral_v<U>, int> onlyForIntegrals() {
        return 42;
    }

    void foo() {}
};

ConditionalMethod<int> integral{};
integral.onlyForIntegrals();
integral.foo();
ConditionalMethod<double> nonIntegral{};
nonIntegral.onlyForIntegrals(); // Compilation error 💥
nonIntegral.foo();

The onlyForIntegrals member function is only available for integral numbers. If we instantiate a ConditionalMethod class with a double, then trying to invoke onlyForIntegrals on that instance will fail to compile.

Let’s take a look at an earlier example where the player gets capabilities from add-ons. Assuming we have created our own CanFly and CanSail traits, we use them to enable certain member functions.

template<typename Addon>
struct Player {
    template<typename U = Addon>
    std::enable_if_t<CanFly<U>::value> fly() { /* Fly */ }

    template<typename U = Addon>
    std::enable_if_t<CanSail<U>::value> sail() { /* Sail */ }
};

Player<Jetpack> playerA{};
playerA.fly();
playerA.sail(); // 💥 Won't compile
Player<Wings> playerB{};
playerB.fly();
playerB.sail(); // 💥 Won't compile
Player<Boat> playerC{};
playerC.fly(); // 💥 Won't compile
playerC.sail();

A Player class instance may have a fly() member function only if the Addon provides flying capabilities. At the same time, a Player class instance may have a sail() member function only if the Addon provides sailing capabilities:

  • A Player with a Jetpack can fly but cannot sail.
  • A Player with a Boat can sail but cannot fly.

This is a powerful technique because we make API misuse obvious at compile-time. We do not need to wait for our code to run to find out that we are doing something wrong. We shouldn’t be flying with a boat, right?
Remember, in this snippet, we did not introduce any new feature. It’s more of a cool use case of SFINAE. What’s most important is that it doesn’t look too bad either.

Fold expressions + SFINAE

The last thing I want to quickly show you, is how to use SFINAE with fold expressions.

template<typename T, typename = void>
struct HasLightAlcoholPermission : std::false_type {};

template<typename T>
struct HasLightAlcoholPermission<T, std::void_t<typename T::ServeBeer>>
    : std::true_type {};

template<typename... Permissions>
struct CanServeLightAlcohol
    : std::disjunction<HasLightAlcoholPermission<Permissions>...> {};

// If the `T::ServeBeer` type exists, the specialization is chosen, or SFINAE

First we create a trait that checks if a type has a ServeBeer member type definition. We assume that the type gives the permission to serve light alcohol. Then we can check multiple types for the permission to serve light alcohol, using a fold expression. Remember, std::disjunction? It is the logical OR of template metaprogramming. So if any of the Permissions allows us to serve light alcohol, then the result is true.

Takeaways

That’s it for this post. I have good news and bad news. The bad news is that there are some more ways to do SFINAE, more ways to get weird compilation errors and get confused. The good thing is that you will not typically encounter them. I think the most complex thing you may see is a fold expression and SFINAE without using traits to make the code more readable.
Hopefully, if you paid enough attention, you will be able to put 2 and 2 together. If not, then don’t worry, chances are you may not need to use SFINAE at all. For example, if concepts are available go for them instead.
Overall, I hope you enjoyed this tutorial and became more comfortable with SFINAE. It’s a very powerful technique. If done right, you use it only when you must and create your own traits, it’s not too ugly either.