SFINAE and Template Overload Resolution

Last update: 2026-01-21 08:29:00 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

C++ substitutes template arguments while it forms function-template candidates. Substitution failure is not an error (SFINAE) means that an invalid dependent type or expression in the immediate context of that substitution removes that candidate; it does not make arbitrary invalid template code safe.

Candidate Formation and Immediate Context #

Overload resolution begins by forming candidates. For a function template, deduction or explicitly supplied arguments can require substitution into its function type and template parameter declarations. If that immediate declaration context is invalid, the specialization is discarded and another viable overload may be selected.

cpp
#include <cstddef>
#include <concepts>
#include <type_traits>

template <class T>
auto element_count(const T& value) -> decltype(value.size()) {
    return value.size();
}

template <class T, std::size_t N>
constexpr std::size_t element_count(const T (&)[N]) {
    return N;
}

For an array, the first candidate fails because an array has no size() member. That failure occurs in its trailing return type, so the array overload remains available. A non-template overload can also win when it is a better match.

SFINAE does not apply after a candidate has been selected merely because its function body fails to compile:

cpp
template <class T>
auto broken(const T& value) {
    return value.missing_member(); // hard error if this specialization is instantiated
}

Put detection in a substituted declaration context, not in an implementation body.

Established Detection Techniques #

std::enable_if_t conditionally supplies a type, which makes it useful in a template parameter, parameter type, or return type.

cpp
template <class T,
          std::enable_if_t<std::is_integral_v<T>, int> = 0>
T twice(T value) {
    return value * 2;
}

decltype tests whether an expression is well formed. std::void_t supports the detection idiom because it becomes void when every supplied type is valid.

cpp
template <class, class = void>
struct has_value_type : std::false_type {};

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

Use std::declval<T>() inside an unevaluated operand such as decltype when an expression needs a value category but no object can or should be constructed.

Prefer Constraints for New Interfaces #

C++20 constraints express eligibility directly and participate in overload resolution. They preserve the same distinction: an invalid expression inside a requires expression evaluates to false in its requirement context, while errors in selected implementation code remain errors.

cpp
#include <concepts>
#include <cstddef>

template <class T>
concept Sized = requires (const T& value) {
    { value.size() } -> std::convertible_to<std::size_t>;
};

template <Sized T>
auto element_count(const T& value) {
    return value.size();
}

Constraints produce declarations that state intent and yield better diagnostics. Keep SFINAE for interfaces that must support pre-C++20 language modes or conventions whose overload set already relies on substitution failure.

Comments

Read or comment on GitHub (sign-in required to comment)