Intuition What this page is
The parent SFINAE note told you the rule . Here we run the machine on every kind of input it can meet: valid substitution, invalid-in-signature, invalid-in-body, the enable_if on/off switch, ambiguous overlaps, empty/degenerate cases, and an exam trap. Each case is a cell in a matrix so you can see nothing is left out.
Before any example, one reminder in plain words. A template is a recipe with a blank T in it. Substitution = the compiler pastes a real type into that blank. The signature is the "header" of a function — its return type and parameter types, the part written before the { }. The body is the { ... } part. SFINAE only watches the header.
Every situation SFINAE can face falls into one of these cells:
#
Cell class
What is fed in
Expected outcome
1
Valid substitution
type that does fit the signature
candidate stays, compiles
2
Failure in signature
type missing a member named in the header
candidate silently dropped (SFINAE)
3
Failure in body
header fine, { } uses a missing member
hard error — NOT SFINAE
4
enable_if = true
condition holds → ::type exists
overload enabled
5
enable_if = false
condition fails → no ::type
overload disabled (SFINAE)
6
Overlapping conditions
two overloads both valid
ambiguous hard error
7
Degenerate / empty input
.size() on an empty container
valid — returns 0, no SFINAE involved
8
Limiting: last candidate removed
no viable overload survives
"no matching function" error
9
Word problem
real logging function choosing paths
correct path picked at compile time
10
Exam twist
enable_if in return type of identical templates
"redefinition" trap
The examples below hit every cell. The label [Cell N] tells you which.
Worked example The happy path
template < typename T >
typename T :: value_type first ( const T & c) { return * c. begin (); }
auto x = first ( std ::vector <int> { 10 , 20 , 30 });
Forecast: what type is x, and what value does it hold? Guess before reading.
Step 1 — Deduce T.
The argument is std::vector<int>, matched against const T&, so T = std::vector<int>.
Why this step? Substitution needs a concrete type first; deduction supplies it.
Step 2 — Substitute into the return type.
typename T::value_type becomes std::vector<int>::value_type, which is int.
Why this step? This is the exact expression SFINAE watches — the return type of the header.
Step 3 — Check validity.
std::vector<int> does have a member type value_type. No failure → candidate survives → compiles.
Why this step? SFINAE only acts on failure ; here there is none, so nothing dramatic happens.
Verify: *c.begin() dereferences the first element 10. So x == 10 and decltype(x) == int. Sanity: a vector's front element is an int; units match. ✅
Worked example The header breaks — silently
Same first template. Now:
first ( 42 ); // 42 is int
Forecast: compile error, or silent skip? And what does the error message say if it errors?
Step 1 — Deduce. T = int.
Why? 42 matched against const T&.
Step 2 — Substitute into return type. typename int::value_type.
Why? Again the header is checked first.
Step 3 — This is invalid: int has no member value_type.
Because this failure is in the signature (return type), SFINAE fires: this candidate is dropped , not screamed at.
Why? The failure is in the immediate context — the part written before {.
Step 4 — No other first overload exists.
With every candidate gone, the call itself fails.
Verify: The compiler prints "no matching function for call to first(int)" , NOT "int has no member value_type". That difference is the proof SFINAE ran: it treated the failure as "this résumé is not viable" rather than "compilation is over." ✅
See Templates and Type Deduction for why deduction happens before substitution.
Worked example The trap that fools everyone
template < typename T >
void poke ( const T & t ) { // signature: (const T&) -> void — always valid!
t. quack (); // body: assumes T has .quack()
}
poke ( std ::string{ "hi" });
Forecast: does this get silently dropped like Example 2, or crash?
Step 1 — Deduce & substitute the signature. T = std::string. Signature is void poke(const std::string&) — perfectly valid.
Why? Only the header is in the SFINAE region.
Step 2 — Candidate SURVIVES substitution.
Nothing in the header mentions .quack(), so there is nothing to fail there.
Why this matters: SFINAE already finished its job — successfully — before the body is even looked at.
Step 3 — Now the body is compiled. t.quack() — std::string has no quack() → hard error .
Why? The body is not in the immediate context; failures there are ordinary errors.
Verify: Error message is "no member named 'quack' in std::string" and compilation stops. Compare with Example 2's "no matching function." Different message class = different mechanism. To make this SFINAE-friendly you must lift .quack() into the header via decltype (see decltype and Trailing Return Types ). ✅
Worked example The switch is ON
template < typename T >
std :: enable_if_t < std :: is_integral_v < T >, const char* > label ( T ) {
return "integer" ;
}
Forecast: for label(5), does the overload exist? What does enable_if_t resolve to?
Step 1 — Evaluate the condition. std::is_integral_v<int> is true.
Why? enable_if's first argument is the on/off boolean.
Step 2 — Look up ::type. std::enable_if_t<true, const char*> is defined as const char* (the true specialization has using type = T;).
Why? Only the true specialization provides a member type.
Step 3 — Signature becomes const char* label(int). Valid → overload enabled.
Why? A real return type appeared, so the header is well-formed.
Verify: label(5) returns the string "integer". The return type is const char*. See Type Traits (std::is_integral, std::void_t) for what is_integral_v tests. ✅
Worked example The switch is OFF
Same label, but call label(3.14) (a double).
Forecast: with only this one overload, does it compile?
Step 1 — Evaluate condition. std::is_integral_v<double> is false.
Step 2 — Look up ::type. std::enable_if<false, const char*> is the primary template struct enable_if {}; — it has no member type.
Why? Naming a member that doesn't exist is a substitution failure.
Step 3 — SFINAE drops the overload. The header couldn't be formed.
Why? The failure occurred while building the return type = signature = immediate context.
Step 4 — No other overload. Call fails.
Verify: Error is "no matching function for call to label(double)" . To handle doubles you add a second overload with !std::is_integral_v<T> — see Example 6 and std::enable_if and Tag Dispatch . ✅
Worked example Two switches both ON at once
template < typename T >
std :: enable_if_t < std :: is_integral_v < T >, void > h ( T ) {} // A
template < typename T >
std :: enable_if_t <sizeof (T) >= 1 , void> h (T) {} // B (almost always true!)
h ( 7 ); // T = int
Forecast: does A win, B win, or something worse?
Step 1 — Test condition A for int. is_integral_v<int> = true → A enabled.
Step 2 — Test condition B for int. sizeof(int) >= 1 = true → B also enabled.
Why this is the bug: the two conditions are not a clean partition — both hold for int.
Step 3 — Overload resolution sees two equally-good candidates. Neither is a better match.
Why hard error, not a pick? Ties in overload resolution are ambiguous calls , an error — the compiler never "just chooses."
Verify: Error is "call to 'h' is ambiguous" . The fix: make B mutually exclusive, e.g. !std::is_integral_v<T>. Then exactly one survives (see Overload Resolution ). ✅
Worked example SFINAE succeeds, runtime value is trivial
Using the parent's getSize:
template < typename T >
auto getSize ( const T & t ) -> decltype (t. size (), size_t {}) { return t. size (); }
template < typename T >
size_t getSize (...) { return 0 ; }
getSize ( std ::vector <int> {}); // an EMPTY vector
Forecast: does the empty vector confuse SFINAE? What number comes out?
Step 1 — Substitute the constrained overload. decltype(t.size(), size_t{}). The expression t.size() is checked for validity , not value . std::vector<int> has .size(), so this is valid regardless of emptiness.
Why? SFINAE asks "does this expression compile?", never "what does it return at runtime?"
Step 2 — Constrained overload survives , ... fallback is worse → constrained one is chosen.
Why? An empty container still has a .size() member.
Step 3 — Run it. t.size() on an empty vector returns 0.
Why? Emptiness is a runtime fact; it never enters the compile-time decision.
Verify: Return value 0, return type size_t. Note: the answer 0 here comes from the real .size(), NOT from the ... fallback (which would also be 0 — a coincidence). The constrained overload won. Contrast with getSize(7), where int has no .size(), SFINAE drops overload A, and the 0 comes from the fallback . ✅
Worked example Everything gets filtered out
template < typename T >
auto onlyHasBegin ( const T & t ) -> decltype (t. begin (), void ()) {}
onlyHasBegin ( 42 ); // int, no .begin()
Forecast: with no fallback overload, what happens?
Step 1 — Substitute. decltype(t.begin(), void()) with T = int → 42.begin() is invalid.
Why? The begin() probe lives in the trailing return type = signature.
Step 2 — SFINAE drops the sole candidate.
Why? Failure in immediate context.
Step 3 — Overload set is now empty.
Why this is the limiting boundary: SFINAE's job is "remove this one," but if it removes the last one there is nothing left to call.
Verify: Error "no matching function for call to onlyHasBegin(int)" — the silent-drop still happened, it just left an empty set. This is the same message as Example 2 and 5, confirming they share one mechanism. ✅
Worked example Real-world: log-with-count vs log-plain
You are writing logIt(x). If x is a container (has .size()), log "N items". Otherwise just log the value. Choose at compile time , zero runtime if.
template < typename T >
auto logIt ( const T & t ) -> decltype (t. size (), std ::string{}) {
return std :: to_string (t. size ()) + " items" ; // path P
}
template < typename T >
std :: string logIt (...) { return "scalar" ; } // path Q
logIt ( std ::vector <int> { 1 , 2 , 3 , 4 }); // (a)
logIt ( 99 ); // (b)
Forecast: what strings do (a) and (b) produce?
Step 1 — Case (a), T = vector<int>. t.size() valid → path P enabled and preferred over ....
Why P over Q? A concrete parameter beats a ... variadic every time.
Step 2 — Run P. .size() is 4, so "4" + " items".
Step 3 — Case (b), T = int. 99.size() invalid → SFINAE drops P. Only ... path Q remains.
Why? Same signature-probe mechanism as Example 2.
Step 4 — Run Q. Returns "scalar".
Verify: (a) → "4 items", (b) → "scalar". No runtime branch was written; the branch is the overload set itself. This is exactly what Concepts (C++20) makes readable with requires. ✅
enable_if in the return type of identical templates breaks
A student writes:
template < typename T > std :: enable_if_t < std :: is_integral_v < T >, void > k ( T ) {}
template < typename T > std :: enable_if_t < ! std :: is_integral_v < T >, void > k ( T ) {}
Forecast: compiles cleanly, or complains?
Step 1 — Look at what distinguishes the two templates. Only the default template arguments implied by the return-type computation differ. Default template arguments and return types are not part of a function template's signature for the purpose of distinctness .
Why this matters: two function templates that differ only in return type can be treated as redeclarations of the same template.
Step 2 — The safe idiom. Move the enable_if into a defaulted non-type template parameter , which is part of the template's identity:
template < typename T , std :: enable_if_t < std :: is_integral_v < T >, int > = 0 > void k ( T ) {}
template < typename T , std :: enable_if_t < ! std :: is_integral_v < T >, int > = 0 > void k ( T ) {}
Why int = 0? We need a valid non-type parameter type when the condition is true; int with default 0 is the idiomatic filler. When the condition is false, enable_if_t has no ::type → SFINAE drops the overload — exactly what we want.
Step 3 — Now the two templates differ in their template parameter lists → distinct → no redefinition.
Verify: For any T, exactly one of is_integral_v<T> / !is_integral_v<T> is true, so exactly one k survives — never ambiguous (contrast Example 6), never a redefinition. This is the canonical exam answer. ✅
Recall Which cells produce which compiler behaviour?
Silent drop happens in cells 2, 5, 8 (signature failure). Hard error in 3 (body), 6 (ambiguous), 10-naive (redefinition). Cells 1, 4, 7, 9 all compile and run. Match message to mechanism: "no matching function" = SFINAE removed candidates; "no member named X" = body error; "ambiguous" = overlapping conditions.
Ask of any failure: "Is it in the header or the heart?" Header → SFINAE (silent). Heart (body) → hard error (loud).
SFINAE — substitution failure is not an error (index 5.2.17) — parent
Templates and Type Deduction
std::enable_if and Tag Dispatch
decltype and Trailing Return Types
Type Traits (std::is_integral, std::void_t)
void_t Detection Idiom
Overload Resolution
Concepts (C++20)