SFINAE and std::enable_if explained
What "substitution failure is not an error" means, how std::enable_if and std::void_t switch template overloads on and off, and how C++20 concepts replace them.
When the compiler tries a function template during overload resolution, it substitutes the deduced types into the template's declaration. If that produces something invalid, it doesn't report an error: it quietly drops that candidate and tries the others. That rule is SFINAE, "substitution failure is not an error", and before C++20 it was the standard way to constrain templates.
std::enable_if_t<condition, T>meansTwhen the condition is true and names no type at all when it's false, which removes the overload.- The detection idiom asks "does this expression compile for T?": a specialization using
std::void_t<decltype(expr)>applies only whenexpris valid. - Only the declaration counts. An error inside a function's body is still a hard error.
In C++20, concepts say the same thing directly (template <std::integral T>, requires), with far clearer error messages. Write concepts in new code, and recognize enable_if and void_t in the many libraries written before them.
Example
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
template <typename T>
std::enable_if_t<std::is_integral_v<T>, std::string> kind(T) { return "integer"; }
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, std::string> kind(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 << kind(7) << ", " << kind(0.5) << ", " << kind('x') << "\n";
std::cout << std::boolalpha << has_size<std::vector<int>>::value << " " << has_size<double>::value << "\n";
return 0;
}
Output:
integer, floating point, integer
true false