SFINAE and Concepts
Substitution Failure Is Not An Error — compile-time overload selection via enable_if, void_t, and the C++20 concepts that replace them.
SFINAEsince C++98When substituting template arguments into a declaration produces an invalid type or expression in the immediate context of that declaration, the overload is silently removed from consideration rather than triggering a hard compiler error.
Overview
SFINAE predates C++11 but became practically useful with <type_traits> in C++11 and std::enable_if. The rule applies exclusively to the immediate context of a function template declaration — the return type, parameter types, and template parameter list. Failures inside function bodies are always hard errors.
C++17 added if constexpr and std::void_t, reducing the need for complex SFINAE. C++20 concepts eliminated most remaining use cases with direct, readable constraint syntax. For new code targeting C++20+, prefer concepts. SFINAE knowledge remains essential for reading legacy code, maintaining C++11/14/17 libraries, and understanding what concepts compile to.
Syntax
std::enable_if — C++11
std::enable_if<condition, T> has a member type = T when condition is true; when false, ::type does not exist, causing substitution failure.
// Three placement positions — all equivalent in effect:
// 1. Return type (cleanest when there's only one overload set)
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T> // C++14: _t/_v helpers
clamp_to_byte(T n) { return static_cast<T>(std::clamp(n, T{0}, T{255})); }
// 2. Default non-type template parameter (preferred for overload sets)
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
T clamp_to_byte(T n) { return static_cast<T>(std::clamp(n, T{0}, T{255})); }
// 3. Extra function parameter (avoids ambiguity with explicit instantiation)
template<typename T>
T clamp_to_byte(T n, std::enable_if_t<std::is_integral_v<T>>* = nullptr) {
return static_cast<T>(std::clamp(n, T{0}, T{255}));
}Pattern 2 is the most common for overloaded function templates because it doesn't change the function's signature visible to callers. Pattern 1 fails when you need two overloads with different constraints but identical parameter lists — both return types would need to be different, which is impossible for overload resolution.
std::void_t — C++17
std::void_t<Types...> maps any well-formed type list to void. Combined with partial specialization and SFINAE, it detects whether expressions are valid:
// Primary template — fallback
template<typename T, typename = void>
struct is_iterable : std::false_type {};
// Specialisation — selected only when T has begin()/end()
template<typename T>
struct is_iterable<T, std::void_t<
decltype(std::begin(std::declval<T>())),
decltype(std::end(std::declval<T>()))
>> : std::true_type {};
static_assert(is_iterable<std::vector<int>>::value);
static_assert(!is_iterable<int>::value);The same pattern detects member functions, nested types, or any expression:
template<typename T, typename = void>
struct has_serialize : std::false_type {};
template<typename T>
struct has_serialize<T, std::void_t<
decltype(std::declval<T>().serialize(std::declval<std::ostream&>()))
>> : std::true_type {};Logical type traits — C++17
C++17 introduced std::conjunction, std::disjunction, and std::negation which short-circuit during instantiation (unlike &&/|| on _v values, which instantiate all arguments):
// C++17: short-circuit — second trait not instantiated if first is false
template<typename T,
std::enable_if_t<
std::conjunction_v<std::is_class<T>, std::is_default_constructible<T>>,
int> = 0>
void make_one() { T{}; }if constexpr — C++17
For branching inside a single function body rather than selecting between overloads, if constexpr is dramatically simpler:
// C++17: replaces paired enable_if overloads when overload selection isn't needed
template<typename T>
auto serialize(T val) -> std::string {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(val);
} else if constexpr (std::is_same_v<T, std::string>) {
return val;
} else {
return val.to_string(); // only instantiated for the matching branch
}
}if constexpr cannot replace SFINAE when you need overload resolution to select between template specialisations — it only branches within a single instantiation.
C++20 Concepts
// Concept definition
template<typename T>
concept Serializable = requires(T val, std::ostream& os) {
{ val.serialize(os) } -> std::same_as<void>;
};
// Three equivalent spellings for constrained function templates:
// Requires clause after template parameter list
template<typename T> requires std::integral<T>
T next_power_of_two(T n);
// Concept as type constraint directly in template parameter
template<std::integral T>
T next_power_of_two(T n);
// Abbreviated function template (C++20)
T next_power_of_two(std::integral auto n);Examples
Enabling class template members conditionally
With SFINAE, you can conditionally enable member functions inside a class template. The member must itself be templated to defer substitution:
template<typename T>
class numeric_buffer {
std::vector<T> data_;
public:
void push(T val) { data_.push_back(val); }
// Only available when T is arithmetic — member must have its own template param
template<typename U = T>
std::enable_if_t<std::is_arithmetic_v<U>, double>
mean() const {
return std::accumulate(data_.begin(), data_.end(), 0.0) / data_.size();
}
};With C++20, use a requires clause on the member directly — no dummy template parameter needed:
template<typename T>
class numeric_buffer {
std::vector<T> data_;
public:
void push(T val) { data_.push_back(val); }
double mean() const requires std::is_arithmetic_v<T> {
return std::accumulate(data_.begin(), data_.end(), 0.0) / data_.size();
}
};The detection idiom — C++17
A general-purpose pattern for detecting whether an expression is valid:
// C++17 detection idiom
template<typename Default, typename AlwaysVoid,
template<typename...> class Op, typename... Args>
struct detector {
using value_t = std::false_type;
using type = Default;
};
template<typename Default, template<typename...> class Op, typename... Args>
struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> {
using value_t = std::true_type;
using type = Op<Args...>;
};
struct nonesuch {};
template<template<typename...> class Op, typename... Args>
using is_detected = typename detector<nonesuch, void, Op, Args...>::value_t;
// Usage
template<typename T>
using reserve_t = decltype(std::declval<T>().reserve(std::declval<std::size_t>()));
template<typename Container>
void maybe_reserve(Container& c, std::size_t n) {
if constexpr (is_detected<reserve_t, Container>::value) {
c.reserve(n);
}
}Perfect-forwarding constructor with SFINAE
A classic real-world case: preventing a forwarding constructor from shadowing the copy constructor when T is the same type or a derived type:
class Widget {
std::string name_;
public:
// C++11/14: disable when T decays to Widget or a subclass of Widget
template<typename T,
std::enable_if_t<
!std::is_base_of_v<Widget, std::decay_t<T>> &&
!std::is_integral_v<std::remove_reference_t<T>>,
int> = 0>
explicit Widget(T&& name) : name_(std::forward<T>(name)) {}
// C++20 equivalent — same semantics, readable at a glance
template<typename T>
requires (!std::derived_from<std::decay_t<T>, Widget>)
&& (!std::integral<std::remove_reference_t<T>>)
explicit Widget(T&& name) : name_(std::forward<T>(name)) {}
};Best Practices
Prefer if constexpr (C++17) over paired overloads with enable_if when you're branching on type properties inside a single function body. The resulting code compiles faster, is easier to read, and produces clearer errors.
Prefer C++20 concepts over enable_if for all new code targeting C++20. Concepts propagate to callers (they appear in documentation, IDE completions, and error messages), whereas enable_if failures produce substitution-error noise.
Use the default non-type template parameter pattern (enable_if_t<..., int> = 0) rather than the return-type pattern when you have multiple overloads with the same parameter types. The return-type position is part of the signature visible to callers of function pointers and makes partial explicit specialisation awkward.
Don't combine enable_if with auto return type deduction — the two interact poorly and the substitution failure may not occur where you expect.
Write _v and _t suffixes (C++14) instead of ::value and ::type. std::is_integral_v<T> is not just shorter; it avoids an extra template instantiation in some implementations.
Common Pitfalls
SFINAE only applies in the immediate context
template<typename T>
void f(T x) {
typename T::value_type v; // hard error for T=int, NOT SFINAE
} // function body is not the immediate context
// Fix: move the constraint to the declaration
template<typename T>
void f(T x, typename T::value_type* = nullptr) { ... } // SFINAE hereOverlapping constraints cause ambiguity
// BUG: both overloads are enabled for int — int is not floating_point,
// but it IS non-const, so both match
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void g(T) {}
template<typename T, std::enable_if_t<!std::is_const_v<T>, int> = 0>
void g(T) {} // ambiguous for int
// Fix: make constraints mutually exclusive, or use concepts (subsumption rules
// allow concepts to partially order overlapping constraints correctly)SFINAE does not apply inside requires expressions — that's intentional
In C++20, an ill-formed expression inside a requires expression evaluates to false, not a hard error. This is the mechanism that makes requires work, but it means logic errors inside requires silently evaluate to false rather than diagnosing the mistake:
template<typename T>
concept HasPush = requires(T t) {
t.push_bakc(0); // typo: 'bakc' — evaluates to false silently, not a compiler warning
};See Also
- Concepts — C++20 named constraints that replace most SFINAE
- Type Traits — the
<type_traits>machinery that powersenable_if - Templates — template instantiation, specialisation, and argument deduction
- constexpr —
if constexpr(C++17) for compile-time branching inside function bodies