SFINAE — Substitution Failure Is Not An Error
When the compiler tries to instantiate a function template, it substitutes the template arguments into the function signature. If that substitution produces an invalid type (like trying to dereference an int), SFINAE says: don't issue a hard error — just remove this template from the overload set and try the next candidate. This rule turns what would be a compile error into a quiet overload rejection, enabling powerful compile-time dispatch. SFINAE was the pre-C++20 mechanism for conditional template enablement; C++20 concepts are its cleaner successor, but SFINAE is pervasive in existing codebases and standard library implementations.
The SFINAE rule
The rule applies during template argument substitution into the immediate context of the function signature (return type, parameter types, template parameter declarations). A substitution failure there is not an error — it simply removes that overload from consideration. A substitution failure inside the function body IS a hard error.
// SFINAE in the return type — using decltype to require T::begin():
template<typename T>
auto size(const T& c) -> decltype(c.begin(), c.end(), std::size_t{}) {
return std::distance(c.begin(), c.end());
}
// Another overload for raw arrays (no begin/end):
template<typename T, std::size_t N>
std::size_t size(const T (&)[N]) {
return N;
}
std::vector<int> v = {1, 2, 3};
int arr[] = {1, 2, 3, 4};
size(v); // first overload: v has begin()/end() — selected
size(arr); // second overload: raw array — selected
// The "wrong" overload for each call silently fails substitution and is removedstd::enable_if — conditional template enablement
std::enable_if<condition, T> has a member type ::type equal to T when the condition is true, and no member type when it's false. Accessing a non-existent member type is a substitution failure — triggering SFINAE and removing the overload silently.
#include <type_traits>
// Only callable for integral types (int, long, char, ...):
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
double_it(T value) {
return value * 2;
}
// Only callable for floating-point types:
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
double_it(T value) {
return value * 2.0;
}
double_it(5); // selects integral overload
double_it(3.14); // selects floating-point overload
// double_it("x"); // BOTH fail substitution → compile error: no matching functionThe _t suffix is a C++14 alias template for::type and _v is a C++17 alias for ::value. Prefer these to reduce verbosity.
void_t — detecting member existence
std::void_t<Ts...> (C++17) is always void. It exists only to trigger SFINAE: if any of the types in its argument list is invalid, the entire expression is a substitution failure. This is the basis of the detection idiom — checking whether a type has a given member, typedef, or operation.
// Primary template: assume T has no serialize():
template<typename T, typename = void>
struct has_serialize : std::false_type {};
// Partial specialization: enabled only if T::serialize() exists:
template<typename T>
struct has_serialize<T, std::void_t<decltype(std::declval<T>().serialize())>>
: std::true_type {};
struct WithSerialize { std::string serialize(); };
struct WithoutSerialize{ int value; };
static_assert(has_serialize<WithSerialize>::value); // true
static_assert(!has_serialize<WithoutSerialize>::value); // trueC++20 concepts — SFINAE without the pain
SFINAE is powerful but notoriously hard to read, write, and maintain. C++20 concepts provide the same capability with dramatically cleaner syntax and much better error messages. If you can target C++20, prefer concepts over SFINAE.
// SFINAE version (C++11/14):
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
double_it(T v) { return v * 2; }
// Concepts version (C++20) — same semantics, much cleaner:
template<std::integral T>
T double_it(T v) { return v * 2; }
// Or with requires:
template<typename T>
requires std::integral<T>
T double_it(T v) { return v * 2; }
// Concepts give readable error messages:
// double_it(3.14);
// error: no matching function — constraints not satisfied: T must satisfy 'integral'Key rules to remember
SFINAE only applies in the immediate context of the function signature — not the body
A failed substitution in the return type, parameter types, or template parameters silently removes the overload. A failure inside the body is a hard error.
std::enable_if removes an overload from the set when the condition is false
enable_if_t<condition, T> has no type member when condition is false, which is a substitution failure, which removes the overload.
std::void_t detects whether an expression is valid — the detection idiom
void_t<decltype(expr)> fails if expr is ill-formed, enabling compile-time introspection of type members and operations.
Prefer C++20 concepts over SFINAE when you can target C++20
Concepts produce readable error messages at the call site. SFINAE errors are often cryptic walls of substitution failures inside the template.