In some situations, we need to makes sure function templates can only be invoked with some specific types. SFINAE (that stands for Substitution Failure Is Not An Error) is a set of rules that specify how compilers can discard specializations from the overload resolution without causing errors. A way to achieve this is with the help of std::enable_if.

Let us look at an example. Suppose we want to write a function template called `product()` that returns the product of its two arguments. We only want to be able to call it with arithmetic types. Using `std::enable_if` we can define such a function as follows:

1 2 3 4 5 6 |
template <typename T, typename = typename std::enable_if_t<std::is_arithmetic_v<T>>> T product(T const t1, T const t2) { return t1 * t2; } |

We can use it to multiply integers or doubles for example, even booleans (bools can be converted to integers, with `true` becoming 1 and `false` becoming 0), but not other types, such as `std::string`.

1 2 3 4 5 6 |
using namespace std::string_literals; auto v1 = product(3, 4); auto v2 = product(13.4, 2.55); auto v3 = product(false, true); auto v4 = product("one"s, "two"s); // error |

The last line above would produce the following compiler error when compiling with Visual Studio:

1 2 |
error C2672: 'product': no matching overloaded function found error C2783: 'T product(const T,const T)': could not deduce template argument for '<unnamed-symbol>' |

SFINAE-based constraints are not the most intuitive code to read. Eventually, they model concepts with template trickery. But C++20 provides concepts as a first-class experience to make metaprogramming simpler and more expressive both to the compiler and developers. So let us look at how we can do the same and better with concepts.

We can start by providing concepts for numerical types (a type that is either integral, or floating-point). You can see these below. Notice that the standard library provides two concepts called `std::integral` and `std::floating_point` in the header `<concepts>`. The implementation below is identical to the standard one.

1 2 3 4 5 6 7 8 |
template <typename T> concept integral = std::is_integral_v<T>; template <typename T> concept floating_point = std::is_floating_point_v<T>; template <typename T> concept numeric = integral<T> || floating_point<T>; |

Having this `numeric` concept available, we can change the definition of the `product()` function to the following:

1 2 3 4 5 |
template <numeric T> T product(T const t1, T const t2) { return t1 * t2; } |

Compiling the lines above would again produce an error for the last invocation of `product()` using `std::string` arguments. This time, the errors yield by the Visual Studio compiler are as follows:

1 2 |
error C2672: 'product': no matching overloaded function found error C7602: 'product': the associated constraints are not satisfied |

But what if we want to extend the function template `product()` so that it works for every type for which the operator* is overloaded? That’s difficult to do with SFINAE but rather straight-forward with concepts. The only thing we need to do is define a concept that expresses that. Below, this concept is called `multiplicative`.

1 2 3 4 5 |
template<typename T> concept multiplicative = requires(const T a, const T b) { { a * b }->T; }; |

The changes to the definition of `product()` are minimal: we just replace `numeric` with `multiplicative`.

1 2 3 4 5 |
template <multiplicative T> T product(T const t1, T const t2) { return t1 * t2; } |

So what can we do to make `product("one"s, "two"s)` compile? We can overload operator* for `std::string`. The following is an implementation that “zips” two strings together. The product of “abc” and “xywz” is “axbycwz”. The actual implementation is not important; this is provided just for the sake of making the example produce some actual values.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
std::string operator*(std::string const& s1, std::string const& s2) { std::string result(s1.length() + s2.length(), '\0'); size_t i = 0; size_t j = 0; while(i < s1.length() && i < s2.length()) { result[j++] = s1[i]; result[j++] = s2[i]; i++; } for (size_t k = i; k < s1.length(); ++k) result[j++] = s1[k]; for (size_t k = i; k < s2.length(); ++k) result[j++] = s2[k]; return result; } |

With this available the code we’ve seen above compiles without errors.

1 2 3 4 5 6 |
using namespace std::string_literals; auto v1 = product(3, 4); auto v2 = product(13.4, 2.55); auto v3 = product(false, true); auto v4 = product("one"s, "two"s); |

And that’s how simple concepts can make the code. More about the advantages of concepts can be found here: Why I want Concepts, and why I want them sooner rather than later.

See also on this topic: