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
};

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
  • constexprif constexpr (C++17) for compile-time branching inside function bodies