Step 7 of 9
SFINAE and enable_if
Concepts arrived in C++20. Before that, templates were constrained with a trick you'll still meet in older code and in almost every library written before 2020, including parts of the standard library: SFINAE, "Substitution Failure Is Not An Error".
The rule: when the compiler tries a function template during overload resolution, it substitutes the deduced types into the template's signature. If that produces something invalid (a type that doesn't exist, an expression that doesn't compile), the compiler doesn't report an error. It just quietly drops that template from the candidates and tries the others. Libraries use this on purpose to switch overloads on and off.
std::enable_if_t<condition, T> (from <type_traits>) is the usual switch: it means T when the condition is true, and names no type at all when it's false, which makes the signature invalid and removes the overload.
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
template <typename T>
std::enable_if_t<std::is_integral_v<T>, std::string> describe(T) { return "integer"; }
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, std::string> describe(T) { return "floating point"; }
// Detection idiom: does T have a size() member?
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<const T&>().size())>> : std::true_type {};
int main() {
std::cout << describe(42) << ", " << describe(2.5) << "\n";
std::cout << has_size<std::string>::value << has_size<int>::value << "\n";
}
integer, floating point
10
How it works
- For
describe(42), both templates are tried withT = int. In the second,std::enable_if_t<false, std::string>doesn't exist, so that overload silently disappears and the first one is chosen.describe("text")would match neither, and that is an error: "no matching function". - The detection idiom has two parts. The general
has_sizesaysfalse. The specialization applies only whenstd::void_t<decltype(...)>is valid, that is, whenstd::declval<const T&>().size()compiles.std::declval<T>()pretends to produce aTinsidedecltypewithout constructing one. Forint, the expression is invalid, the specialization is dropped, and the general version answersfalse. - Only the signature counts. An error inside a function's body is still a hard error; SFINAE only applies during substitution into the declaration.
The same thing with concepts
template <std::integral T> std::string describe(T) { return "integer"; }
template <typename T> concept HasSize = requires(const T& t) { t.size(); };
Concepts say the same thing directly, and their error messages name the requirement that failed instead of listing every rejected overload. Write concepts in new code, and recognize enable_if and void_t when you read older code.
Your turn: without concepts or requires, write the detection trait has_begin<T> (true when std::declval<const T&>().begin() is valid), and two count_items overloads using enable_if_t: for types with begin(), return the number of elements (std::distance(c.begin(), c.end())); for everything else, return 1.
Previous: Value categories and reference collapsing Next: CRTP and static polymorphism