Skip to content
C++
Language
since C++11
Advanced

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++98

When 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.

cpp
// 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:

cpp
// 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:

cpp
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):

cpp
// 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:

cpp
// 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

cpp
// 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:

cpp
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:

cpp
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:

cpp
// 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:

cpp
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

cpp
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 here

Overlapping constraints cause ambiguity

cpp
// 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:

cpp
template<typename T>
concept HasPush = requires(T t) {
    t.push_bakc(0);  // typo: 'bakc' — evaluates to false silently, not a compiler warning
};

void_t and the detection idiom

std::void_t turns expression validity into a boolean type trait. It maps any well-formed pack of types to void; if any type in the pack is ill-formed, substitution fails and the partial specialisation is dropped. This makes it the building block for the detection idiom.

cpp
// C++17 — canonical void_t trait with primary + specialisation pattern
template<typename T, typename = void>
struct has_value_type : std::false_type {};

template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>>
    : std::true_type {};

static_assert( has_value_type<std::vector<int>>::value);
static_assert(!has_value_type<int>::value);

Stacking multiple expressions in void_t requires ALL of them to be well-formed — it acts as a conjunction:

cpp
// C++17 — only matches when T has both push_back and size
template<typename T, typename = void>
struct is_sequence_container : std::false_type {};

template<typename T>
struct is_sequence_container<T, std::void_t<
    decltype(std::declval<T>().push_back(std::declval<typename T::value_type>())),
    decltype(std::declval<T>().size())
>> : std::true_type {};

The full detection idiom wraps void_t in a reusable detector helper (shown in the Examples section) that also exposes the detected type, not just a boolean.

std::enable_if_t patterns

enable_if_t<Cond, T> is the workhorse of C++11/14 SFINAE — learn its three placement sites and when each is appropriate. The _t alias (C++14) drops the ::type accessor and is strictly preferred.

cpp
// C++14 — enable_if_t in return type position
// Works cleanly for a single overload; breaks for pairs sharing the same params.
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
reciprocal(T x) { return T{1} / x; }  // C++14

// C++14 — enable_if_t as a defaulted non-type template parameter
// Preferred for overload sets: the return type stays clean and unambiguous.
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T reciprocal(T x) { return T{1} / x; }

template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
double reciprocal(T x) { return 1.0 / x; }

A subtle rule: two defaulted non-type parameters of the same type (int = 0) make the two overloads identical in template signature unless the conditions are mutually exclusive — the compiler sees enable_if_t<A, int> = 0 and enable_if_t<B, int> = 0 as the same parameter, so at most one must be well-formed at any call site.

cpp
// C++14 — enable_if_t as a function parameter (avoids explicit-instantiation issues)
template<typename T>
T safe_cast(double v, std::enable_if_t<std::is_integral_v<T>>* = nullptr) {
    // Rounds toward zero before casting
    return static_cast<T>(v);
}

The function-parameter placement is rarely needed in modern codebases but appears in older library headers where explicit instantiation semantics mattered.

SFINAE with trailing return types

Trailing return types (-> expr) are evaluated in the immediate context, making them a natural SFINAE hook for expression-validity checks. This avoids the extra indirection of a helper trait for simple one-off checks.

cpp
// C++11 — trailing return type triggers SFINAE for operator+
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}
// add(1, 2.5) -> double; add("x", 1) -> substitution failure (not a hard error)

decltype in the trailing position can validate an arbitrarily complex expression, including member access, conversion, and nested calls:

cpp
// C++11 — only participates in overload resolution when T::to_string() exists
template<typename T>
auto stringify(const T& val)
    -> decltype(val.to_string(), std::string{})
{
    return val.to_string();
}
// The comma operator: decltype evaluates both operands but the
// resulting type is the last one (std::string). The first expression
// exists purely to trigger SFINAE if to_string() is absent.

Trailing decltype with comma is idiomatic for "check expression X, return type Y" and predates void_t. Prefer void_t / concepts for new trait types; keep the trailing-decltype form for small inline checks in function signatures.

From SFINAE to Concepts: migration table

Concepts are not merely syntactic sugar — they participate in overload ordering via subsumption, which SFINAE cannot do. The table below maps the most common SFINAE patterns to their C++20 concept equivalents.

cpp
// C++11/14 SFINAE                          // C++20 Concepts equivalent
// ──────────────────────────────────────────────────────────────────────
// std::enable_if_t<std::is_integral_v<T>>  // std::integral<T>
// std::enable_if_t<std::is_floating_point_v<T>> // std::floating_point<T>
// std::enable_if_t<std::is_convertible_v<From,To>> // std::convertible_to<From,To>
// std::enable_if_t<std::is_same_v<T,U>>    // std::same_as<T,U>
// std::enable_if_t<std::is_base_of_v<B,D>> // std::derived_from<D,B>

// Custom trait via void_t:
template<typename T, typename = void>
struct has_push_back : std::false_type {};
template<typename T>
struct has_push_back<T, std::void_t<
    decltype(std::declval<T>().push_back(std::declval<typename T::value_type>()))
>> : std::true_type {};

// C++20 concept equivalent — inline, no helper struct needed:
template<typename T>
concept HasPushBack = requires(T c, typename T::value_type v) {  // C++20
    c.push_back(v);
};

Subsumption means concepts can partially order overloads where SFINAE cannot. If concept B refines concept A (i.e., B requires A plus more), a function constrained with B is automatically more specialised than one constrained with A:

cpp
// C++20 — subsumption selects the more constrained overload automatically
template<std::ranges::range R>
void process(R& r) { /* generic path */ }

template<std::ranges::sized_range R>   // sized_range subsumes range
void process(R& r) { /* fast path: size known at O(1) */ }

std::vector<int> v{1,2,3};
process(v);  // calls sized_range overload — no ambiguity, no extra tricks needed

Concept diagnostics and error messages

One of the primary motivations for concepts is human-readable error messages — SFINAE errors are notoriously verbose. When a concept constraint fails, the compiler reports which concept was not satisfied rather than a chain of template substitution failures.

cpp
// C++20 — unsatisfied concept produces a clear diagnostic
template<std::integral T>
T double_it(T x) { return x * 2; }

// double_it(3.14);
// error: 'double_it' requires 'std::integral<double>' which is not satisfied
// — rather than 5 screens of "candidate template ignored: substitution failure"

To write concepts that produce maximally informative errors, decompose compound requirements into named sub-concepts and use requires requires only as a last resort:

cpp
// C++20 — layered concepts give targeted diagnostics
template<typename T>
concept Addable = requires(T a, T b) { a + b; };

template<typename T>
concept Printable = requires(T v, std::ostream& os) { os << v; };

template<typename T>
concept Loggable = Addable<T> && Printable<T>;   // C++20

template<Loggable T>
void log_sum(T a, T b) {
    std::cout << (a + b) << '\n';
}
// log_sum(std::mutex{}, std::mutex{});
// error: 'Loggable<std::mutex>' not satisfied because 'Addable<std::mutex>' not satisfied

Avoid negated concepts as the primary constraint when a named positive concept exists — !Foo<T> does not subsume anything and gives the compiler nothing to say beyond "constraint not satisfied".

SFINAE on non-type template parameters

Non-type template parameters (NTTPs) can also participate in SFINAE — enabling or disabling overloads based on integral values, pointers, or (C++20) structural types. This is less common than type-based SFINAE but arises in fixed-size containers, policy classes, and bitflag APIs.

cpp
// C++11 — enable overload only for sizes that are powers of two
template<std::size_t N,
    std::enable_if_t<(N & (N - 1)) == 0, int> = 0>   // C++14
struct aligned_storage {
    alignas(N) unsigned char data[N];
};

// aligned_storage<16> ok; aligned_storage<3> -> substitution failure

With C++20, the same constraint is expressed as a requires clause on the NTTP directly:

cpp
// C++20 — requires clause constrains the non-type parameter
template<std::size_t N> requires ((N & (N - 1)) == 0)
struct aligned_storage {
    alignas(N) unsigned char data[N];
};

C++20 also allows floating-point and user-defined structural types as NTTPs, expanding SFINAE / concept constraints to a broader value domain:

cpp
// C++20 — structural type as NTTP with concept constraint
template<typename T>
concept Structural = std::is_trivially_copyable_v<T>
                  && std::is_standard_layout_v<T>;

template<Structural auto Val>   // C++20: NTTP of a concept-constrained type
struct compile_time_constant {
    static constexpr auto value = Val;
};

compile_time_constant<42>    c1;  // int NTTP — ok
compile_time_constant<3.14>  c2;  // double NTTP — ok in C++20

See Also

  • Concepts — C++20 named constraints that replace most SFINAE
  • Type Traits — the <type_traits> machinery that powers enable_if
  • Templates — template instantiation, specialisation, and argument deduction
  • constexpr — if constexpr (C++17) for compile-time branching inside function bodies