5.2.17 · D5C++ Programming

Question bank — SFINAE — substitution failure is not an error

1,813 words8 min readBack to topic

For the machinery itself, keep SFINAE — substitution failure is not an error open, plus Templates and Type Deduction and decltype and Trailing Return Types.


First, three ideas the questions lean on

Before the bank, we build the three terms every trap uses, from zero.

Picture the declaration as a house: the signature is the front door sign (checked before you enter), the body is the interior (checked only after you walk in).


True or false — justify

The compiler prints an error every time template substitution fails.
False — a failure in the signature is silently removed from the overload set (that's the whole point of SFINAE); only if no candidate survives do you get "no matching function".
If a substitution failure happens inside the function body, SFINAE still saves you.
False — the body is not the immediate context, so a failure there is a hard error that stops compilation.
std::enable_if<false, int>::type is simply int.
False — the primary template has no ::type, so false gives nothing; naming ::type is the substitution failure that drops the overload.
std::enable_if<true, R>::type is R.
True — the partial specialization for true defines using type = R;, so the alias resolves to R.
Two enable_if overloads may both be simultaneously valid for a type; the compiler just picks the better one.
False — if both survive with the same signature it is an ambiguous call, a hard error; the conditions must be a clean partition (see figure s02).
A variadic ... fallback overload will win even when a constrained overload is also viable.
False — ... is the lowest-priority match on the conversion ladder (figure s03), so it only wins when every better-matching candidate has been SFINAE'd away.
SFINAE and if constexpr are two names for the same mechanism.
False — SFINAE removes candidates during overload resolution; if constexpr discards branches inside one function body after the overload is already chosen.
Concepts (C++20) are just prettier syntax with no behavioural difference from enable_if.
Mostly false — they also give better error messages and order overloads by subsumption (the more-constrained one wins, figure s04); see Concepts (C++20).
decltype(t.size(), size_t{}) returns whatever t.size() returns.
False — the comma operator evaluates t.size() only for validity and yields the right-hand type, so the return type is size_t, not t.size()'s type.
Putting std::enable_if differences only in the return type of two otherwise-identical templates is always safe.
False — that can trigger "redefinition"/signature-clash issues; the robust idiom is a defaulted non-type template parameter std::enable_if_t<cond,int> = 0.

Spot the error

Why does this "fail nicely" plan not work? template<class T> void f(T t){ t.foo(); } — "if T has no foo, SFINAE drops it."
The call t.foo() is in the body (the house interior, not the door sign), not the immediate context, so there is nothing to SFINAE — it is a hard error. Move the check into the signature via decltype(t.foo()) or enable_if.
std::enable_if_t<cond> with no second argument — what type is the enabled overload's return?
enable_if's default second parameter is void, so enable_if_t<true> is void; fine for a void-returning overload, but you must not forget it defaults to void.
enable_if_t<is_integral_v<T>,void> f(T); and enable_if_t<is_integral_v<T>,void> f(T); — what's wrong?
Both conditions are identical, so for every integral T both survive → the two regions overlap → ambiguous (and effectively a redefinition). The second should be !is_integral_v<T> to partition the space (figure s02).
auto g(const T& t) -> decltype(t.size()) returning getSize — why might this still hard-error?
Only t.size() (in the signature) is protected. If the body uses another unchecked member (e.g. t.capacity()), that member's absence is outside the immediate context → a hard error, not SFINAE.
A detector uses T::value_type in the body to decide behaviour and expects a fallback. Where does it break?
In the body it's a hard error for types lacking value_type; the check must live in the signature / immediate context (trailing decltype, enable_if, or void_t Detection Idiom).

Why questions

Why does SFINAE exist at all — what would we lose without it?
Without it, the first ill-formed candidate the compiler tried would kill the whole compile; SFINAE lets a failed substitution be a silent question — "does this type support X?" — enabling compile-time branching.
Why must the failure be in the immediate context and not the body?
The compiler substitutes only into the signature (parameter list/defaults, return type, parameter types) while selecting candidates; the body is compiled later, after a candidate is already chosen, so a body error has no "other candidate" to fall back to.
Why is decltype(expr, ReturnType{}) a common SFINAE trick rather than just decltype(expr)?
The comma operator lets you validity-check expr while still returning a chosen type (ReturnType), decoupling "does this compile?" from "what do I return?".
Why do overlapping enable_if conditions cause an error instead of a pick?
Overload resolution needs a unique best match; two equally-good survivors is genuinely ambiguous (an overlap region in figure s02), so the standard makes it an error rather than an arbitrary choice.
Why is a ... parameter the classic fallback?
On the conversion-ranking ladder (figure s03) the ellipsis conversion ranks below every other conversion, so the fallback is chosen only when all constrained overloads have been removed — exactly the "when nothing else fits" role.
Why did C++20 Concepts partly replace this pattern?
enable_if machinery is dense and its errors are cryptic; Concepts express the same constraints readably, order overloads by subsumption (more-constrained wins, figure s04), and report which requirement failed.

Edge cases

What happens if SFINAE removes the only candidate for a call?
You get the generic "no matching function for call"not the underlying "int has no value_type" error, which can be confusing to debug.
Is failure to deduce a template argument (before any substitution) SFINAE?
It's the same family of "candidate not viable, move on" behaviour — a failed deduction also just removes the candidate silently rather than erroring.
Does a substitution failure in a default template argument count as SFINAE?
Yes — template parameter defaults are part of the immediate context, so an invalid default drops the candidate silently.
If enable_if differences are placed in a non-type template parameter default, are the two overloads considered different?
They differ by that parameter, so they are distinct declarations; combined with mutually exclusive conditions, exactly one survives — this is the recommended idiom.
For a type where an expression is ill-formed only inside a nested requirement of the body, will SFINAE kick in?
No — anything below the signature is not immediate context; only what appears in the parameter list/defaults, return type, or parameter types is SFINAE-protected.

Connections