Exercises — SFINAE — substitution failure is not an error
A tiny reminder of the vocabulary you'll need, so no symbol is unearned:
Related tools you may reach for: 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).
Level 1 — Recognition
L1.1 — Signature or body?
For each snippet, say whether the failure is SFINAE (silently removes the candidate) or a HARD error (stops compilation).
// (a)
template <typename T>
typename T::value_type a(const T&); // called with T = int
// (b)
template <typename T>
void b(const T&) { typename T::value_type x; } // called with T = intRecall Solution
- (a) The illegal thing
int::value_typeis in the return type = the signature. → SFINAE. Candidateais silently dropped. If it was the only candidate you get "no matching function". - (b) The signature
void b(const T&)is perfectly valid forT = int. The illegalint::value_typeis inside the body. The body is not the immediate context → HARD error. One-line rule: broken header = toss the résumé; broken inside = interview crashes.
L1.2 — What is ::type?
State the value (or non-existence) of:
std::enable_if<true, double>::type and std::enable_if<false, double>::type.
Recall Solution
enable_if<true, double>::type=double(thetruepartial specialization definesusing type = T;).enable_if<false, double>::type= does not exist. The primary template has no::type, so naming it is a substitution failure → drops the overload.
Level 2 — Application
L2.1 — Predict the output
#include <cstdio>
template <typename T>
auto f(const T& t) -> decltype(t.size(), int{}) { return 1; } // A
template <typename T>
int f(...) { return 2; } // B
int main() {
std::printf("%d %d\n", f(std::string("hi")), f(42));
}What does it print, and why each number?
Recall Solution
Prints 1 2.
f(std::string("hi")):std::stringhas.size(), sodecltype(t.size(), int{})is valid → A survives and is a better match than the variadic...of B → A wins → 1.f(42):inthas no.size(), so A's return type is a substitution failure → A is dropped → only B (variadic, worst match, always valid) remains → B wins → 2. The comma operator insidedecltype:t.size()is evaluated only for validity, then discarded; the wholedecltypeyieldsint{}'s type,int.
L2.2 — Fix the overlap
template <typename T>
std::enable_if_t<std::is_integral_v<T>, void> g(T); // A
template <typename T>
std::enable_if_t<sizeof(T) >= 1, void> g(T); // BCall g(5). What goes wrong and how do you fix it?
Recall Solution
For T = int: condition A is true (int is integral) and condition B is sizeof(int) >= 1 is also true. Both overloads survive → ambiguous call → hard error, not "compiler picks one."
Fix: make the conditions a clean partition. Replace B's condition with the negation of A:
template <typename T>
std::enable_if_t<!std::is_integral_v<T>, void> g(T); // B fixedNow exactly one of A / B is viable for every T.
Level 3 — Analysis
L3.1 — Why does return-type enable_if on twins break?
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void h(T);
template <typename T, typename = std::enable_if_t<!std::is_integral_v<T>>>
void h(T);These two look different (opposite conditions). But the compiler may reject them with "redefinition / cannot be overloaded." Explain why, and give the safe idiom.
Recall Solution
Why it breaks: a default template argument is not part of the function's signature for the purpose of distinguishing overloads. Both templates reduce to the same signature template<class T, class> void h(T). Two templates with identical signatures = redeclaration of the same template → error, before SFINAE ever runs.
Safe idiom — put the enable_if in a non-type template parameter that participates in substitution:
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void h(T);
template <typename T, std::enable_if_t<!std::is_integral_v<T>, int> = 0>
void h(T);Here std::enable_if_t<cond,int> is the type of a defaulted non-type parameter. When cond is false, that type doesn't exist → SFINAE drops the overload cleanly, and the two remain distinct.
L3.2 — Trace the drop
For getSize from the parent note:
template <typename T>
auto getSize(const T& t) -> decltype(t.size(), size_t{}) { return t.size(); } // A
template <typename T> size_t getSize(...) { return 0; } // BList the exact substitution steps for getSize(3.14) (a double).
Recall Solution
- Deduce
T = doublefromconst T& tmatching thedoubleargument. - Substitute into A's return type:
decltype( (3.14).size(), size_t{} ). The sub-expressiont.size()requiresdouble::size()—doublehas no membersize→ substitution failure in the immediate context (return type). - SFINAE: A is silently removed from the overload set.
- B has parameter
...(always viable, worst rank) and no constraint → B is the only survivor → chosen. getSize(3.14)returns0. No hard error, no message about "double has no size."
Level 4 — Synthesis
L4.1 — Build a has_begin detector with void_t
Using the void_t Detection Idiom, write a trait has_begin<T> that is true when T has a member begin() and false otherwise. Then explain each moving part.
Recall Solution
#include <type_traits>
// Primary: assume NO begin. Second param defaulted to void.
template <typename T, typename = void>
struct has_begin : std::false_type {};
// Specialization: only viable when decltype(...) is a valid type -> void.
template <typename T>
struct has_begin<T, std::void_t<decltype(std::declval<T&>().begin())>>
: std::true_type {};Moving parts:
std::void_t<...>maps any valid type list tovoid. Its whole job is to be a "did this expression compile?" switch.decltype(std::declval<T&>().begin()):declval<T&>()conjures aT&value without constructing one (we only need its type in the unevaluateddecltype). IfThas nobegin, this is a substitution failure — but it's in the specialization's template argument, so SFINAE just makes the specialization non-viable.- When
.begin()is valid → the second argument isvoid, which matches the primary's default= void, so the specialization (more specialized) is chosen → inheritstrue_type. - When
.begin()is invalid → specialization drops out → primary (false_type) is used. Usage:has_begin<std::vector<int>>::valueistrue;has_begin<int>::valueisfalse.
L4.2 — Same idea with a Concept (C++20)
Rewrite the L4.1 detector's intent using a concept and constrain a function.
Recall Solution
#include <concepts>
template <typename T>
concept HasBegin = requires (T& t) { t.begin(); }; // does t.begin() compile?
template <HasBegin T> void use(T& t) { /* iterate */ }
template <typename T> void use(T&) { /* fallback */ }The requires(T& t){ t.begin(); } block is exactly the SFINAE test made readable: it is true iff t.begin() is a well-formed expression. Concepts do the same removal from the overload set that SFINAE does — but the constraint is named and the error messages point at HasBegin instead of a cryptic no-match.
Level 5 — Mastery
L5.1 — Three-way dispatch with a clean partition
Write three overloads of describe(T):
- integral types → returns
1 - floating-point types → returns
2 - everything else → returns
3
using enable_if, guaranteeing no ambiguity for any T. Then predict describe(7), describe(2.0), describe("hi").
Recall Solution
#include <type_traits>
template <typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
int describe(T) { return 1; }
template <typename T,
std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
int describe(T) { return 2; }
template <typename T,
std::enable_if_t<!std::is_integral_v<T> && !std::is_floating_point_v<T>, int> = 0>
int describe(T) { return 3; }The three conditions are a partition: integral, floating, and "neither." For any T exactly one is true, so exactly one overload survives — no ambiguity.
describe(7)→7isint(integral) → 1.describe(2.0)→2.0isdouble(floating) → 2.describe("hi")→ the literal"hi"isconst char[3], neither integral nor floating → 3. (See the partition diagram below — every type falls in exactly one region.)

L5.2 — Debug the crash
A student writes a "detector" that always hard-crashes:
template <typename T>
struct is_addable {
static constexpr bool value = requires { std::declval<T>() + std::declval<T>(); };
T probe = std::declval<T>() + std::declval<T>(); // <-- line X
};Identify the exact line that makes it a hard error even for T = int, and fix the detector.
Recall Solution
Line X is the bug: T probe = std::declval<T>() + std::declval<T>(); is an evaluated member initializer inside the struct body. Two problems:
std::declval<T>()may only appear in unevaluated contexts (decltype,sizeof,requires,noexcept) — using it in a real initializer is ill-formed for everyT, includingint.- Even if it compiled, the initializer is in the body, so a non-addable
Twould give a hard error, not a cleanfalse. Fix — keep the probe unevaluated, drop the member:
template <typename T>
struct is_addable {
static constexpr bool value =
requires (T a, T b) { a + b; }; // pure compile-time test, no body probe
};Now is_addable<int>::value == true, is_addable<std::mutex>::value == false, and no hard error either way.
Recall One-line summaries to self-quiz
Where must the failure live for SFINAE? ::: In the signature (immediate context) — never the body.
Two equally-viable SFINAE overloads → ? ::: Ambiguous call = hard error.
How do you make twin overloads distinct? ::: std::enable_if_t<cond,int> = 0 as a non-type param, not a default.
What does std::void_t<decltype(expr)> test? ::: Whether expr is a well-formed type/expression.
Where may std::declval<T>() appear? ::: Only in unevaluated contexts (decltype/sizeof/noexcept/requires).
Connections
- Parent: SFINAE
- 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)