This page is the "throw every ball at it" drill for C++20 concepts . We will not just use concepts — we will hunt down every kind of situation a constraint can face and work each one fully, so you never meet a case you haven't already seen resolved.
Intuition How to read the coloured boxes on this page
Throughout the vault, coloured callout boxes carry a fixed meaning so your eye can skim:
[!intuition] — the plain-words mental picture, no code required.
[!definition] — the precise meaning of a term the first time it appears.
[!example] — a fully worked problem (statement → forecast → steps → verify).
[!mistake] — a tempting wrong idea, why it feels right, and the fix.
[!recall]- — a collapsible self-test (click to expand).
[!mnemonic] — a memory hook.
They are formatting only — the content is what matters; the colour just tells you which mode you're in.
Intuition What "every scenario" means for concepts
A concept is a compile-time yes/no question about a type . So the "cases" are not quadrants of a circle — they are the different ways a type can answer that question : it satisfies, it fails, it partially satisfies (some operations exist, others don't), two concepts both say yes (subsumption), nobody says yes (hard error), and the degenerate edges (empty range, void, a type that lies about its operations). We enumerate them first, then knock each one down.
Think of each row as one "cell" the topic can hand you. Every worked example below is tagged with the cell(s) it covers.
#
Cell class
Concrete trigger
What we must show
C1
Satisfied
int vs Numeric
constraint passes, body compiles
C2
Fails, but another overload still matches
std::string between Numeric and a fallback
one candidate dropped, the other is chosen
C3
All fail → hard error
no candidate survives
the clean "no matching function" message
C4
Two overloads, one applies
int between integral/floating_point
disjoint constraints, exactly one wins
C5
Two overloads, both apply → subsumption
int between Integral and SignedIntegral
more-constrained wins
C6
Ambiguous, neither subsumes
two unrelated requires
compiler error: ambiguous call
C7
Compound requirement (type of result)
{ a+b } -> same_as<T>
expression valid and returns T
C8
Nested / word problem
a real "printable range"
build a multi-part concept, real container
C9
Degenerate inputs
empty range, void, self-referential type
limiting behaviour, no crash
C10
Exam twist
requires requires, short-circuit of `
We now cover C1–C10 across 10 examples (one per cell).
int satisfy Numeric, and does sq compile?
template < typename T >
concept Numeric = std ::integral < T > || std ::floating_point < T > ;
template < Numeric T > T sq ( T x ) { return x * x; }
sq ( 6 ); // T = int
Forecast: guess — does this compile, and what does sq(6) return?
Evaluate the predicate Numeric<int>.
Why this step? A constraint is checked before the body. std::integral<int> is true, so the || short-circuits to true. We never even look at std::floating_point<int>.
Constraint satisfied ⇒ int is admitted; the body x * x is compiled.
Why this step? Only now does the compiler try to typecheck x * x. int * int is int, so it's fine.
Result: sq(6) = 6 * 6 = 36, of type int.
Verify: int has operator*, so had we skipped the concept it would still compile — the concept added no runtime cost, only a guard. Answer = 36 . ✔
Worked example With TWO overloads, what happens to
handle(std::string{"hi"})?
template < Numeric T > const char* handle ( T ){ return "numeric" ; } // (A)
template < typename T > const char* handle ( T ){ return "generic" ; } // (B) unconstrained fallback
handle ( 6 ); // ?
handle ( std ::string{ "hi" }); // ?
Forecast: does handle(std::string{"hi"}) error, or fall back? And which wins for handle(6)?
handle(6): filter both. Numeric<int>=true ⇒ (A) survives; the unconstrained (B) always survives.
Why this step? An unconstrained template has no constraint to fail, so it is always a candidate.
Both survive ⇒ subsumption ranks them. (A) carries the constraint Numeric, (B) carries none ; a constrained candidate is more-constrained than an unconstrained one, so (A) wins ⇒ handle(6) = "numeric".
Why this step? This is the C5 tie-break rule applied to "some constraint vs no constraint".
handle(std::string{"hi"}): filter both. Numeric<std::string>=false ⇒ (A) is silently removed . (B) still survives.
Why this step? An unsatisfied constraint drops one candidate — it does not error, precisely because another candidate remains to catch the call.
One survivor ⇒ (B) is called ⇒ handle(std::string{"hi"}) = "generic". No error.
Verify: The dropped-(A) case is the true "C2" scenario: a failing concept quietly steps aside and the fallback answers. Results: handle(6)="numeric", handle("hi")="generic". ✔
Worked example Now REMOVE the fallback — what does
sq(std::string{"hi"}) do?
template < Numeric T > T sq ( T x ) { return x * x; } // the ONLY sq
sq ( std ::string{ "hi" });
Forecast: will the error mention operator* deep inside sq, or something else?
Check Numeric<std::string>. std::integral<std::string>=false; std::floating_point<std::string>=false; false || false = false.
Why this step? The concept is a pure boolean — we compute it with no reference to the body.
Constraint fails ⇒ this sq is removed from the candidate set.
Why this step? An unsatisfied constraint deletes the candidate without ever diving into the body to look for operator*.
Now ZERO candidates remain ⇒ the compiler reports "no matching function for call to sq" — at the call site , not at return x * x.
Why this step? This is the defining feature of C3: the error only appears when every candidate has been dropped, and it is short and points where you called.
Verify: Contrast the pre-C++20 duck-typed template<typename T> T sq(T x){return x*x;} — that would compile the body and vomit an error inside std::string's operator* lookup. Same failing input, radically cleaner message. The difference between C2 and C3 is exactly whether any other candidate survives . ✔
describe runs for 5 and for 3.14?
template < std :: integral T > void describe ( T ){ /* "int" */ } // (A)
template < std :: floating_point T > void describe ( T ){ /* "real" */ } // (B)
describe ( 5 ); // ?
describe ( 3.14 ); // ?
Forecast: for describe(5), which overloads survive the constraint check?
describe(5): test both constraints. integral<int> = true ⇒ (A) survives. floating_point<int> = false ⇒ (B) removed.
Why this step? Each overload is filtered independently by its own constraint before overload resolution ranks anything, so int's failure of floating_point cleanly drops (B) with no error.
One survivor ⇒ (A) is called. No ambiguity because (B) was never a candidate.
Why this step? With a single survivor there is nothing to rank — resolution ends immediately.
describe(3.14): integral<double> = false ⇒ (A) removed; floating_point<double> = true ⇒ (B) called.
Why this step? Same filter, opposite outcome — the two constraints are disjoint , so at most one is ever admitted.
Verify: integral and floating_point are mutually exclusive for arithmetic types, so no input can trigger both. There is no cell here where both survive — that's a different cell (C5), handled next. ✔
int, both Integral and SignedIntegral are true — which overload wins?
template < class T > concept Integral = std ::integral < T > ;
template < class T > concept SignedIntegral = Integral < T > && std ::is_signed_v < T > ;
template < Integral T > const char* which ( T ){ return "integral" ; } // (P)
template < SignedIntegral T > const char* which ( T ){ return "signed-integral" ; } // (Q)
which ( 7 ); // int is BOTH integral and signed
Forecast: does this fail as ambiguous, or does one win? Why?
The figure above shows this as two circles: the big violet circle is Integral (all integer types), and the smaller magenta circle inside it is SignedIntegral. The navy dot is int, sitting inside both . The orange arrow points to the rule: the inner (stronger) circle wins .
Both constraints pass for int: Integral<int>=true and SignedIntegral<int>=true. So both (P) and (Q) survive filtering — the navy dot is in both circles.
Why this step? Now we're in the C5 cell — the two survivors force the compiler to order them.
Compare constraints by subsumption. In the figure the magenta circle is drawn inside the violet one because SignedIntegral is literally defined as Integral<T> && (extra). It contains Integral as a sub-term, so it demands strictly more . We say SignedIntegral subsumes Integral.
Why this step? Subsumption is the tie-breaker: "if every requirement of (Q) implies a requirement of (P) and (Q) asks for more, (Q) is more specialized."
More-constrained wins ⇒ (Q) which(7) returns "signed-integral".
Why this step? Following the orange arrow: given two survivors, the compiler keeps the inner/stronger one.
Verify: For an unsigned type like unsigned u = 7u: SignedIntegral<unsigned> = false (fails is_signed), so only (P) survives ⇒ returns "integral". The same code base cleanly handles both — no enable_if gymnastics. ✔
Common mistake Subsumption only works when concepts share terms
SignedIntegral subsumes Integral because it literally names Integral<T> inside it. If you wrote two independent std::is_signed_v && std::integral expansions that happen to be equivalent, the compiler may not see the containment and you get C6 (ambiguity). Build stronger concepts out of weaker ones — see Example 6.
Worked example Two unrelated requirements that both pass — what error?
template < class T > concept HasPlus = requires (T a){ a + a; };
template < class T > concept HasMinus = requires (T a){ a - a; };
template < HasPlus T > void g ( T ){} // (X)
template < HasMinus T > void g ( T ){} // (Y)
g ( 3 ); // int has BOTH + and -
Forecast: will the compiler pick one, or refuse?
Both survive: HasPlus<int>=true, HasMinus<int>=true.
Why this step? Two survivors again → we must order them.
Try subsumption. HasPlus and HasMinus share no common sub-expression — neither's requirement set contains the other's. Neither subsumes.
Why this step? Subsumption needs a syntactic containment relationship; disjoint requires bodies have none.
No ordering ⇒ ambiguous call — compile error.
Why this step? Unlike C2/C3, this is a legitimate hard error: the language cannot decide, and refuses to guess.
Verify: The fix is to build one from the other, e.g. concept HasBoth = HasPlus<T> && HasMinus<T>; and make (Y) require HasBoth. Then HasBoth subsumes HasPlus, and C6 collapses into C5 (unambiguous). The distinction between C5 and C6 is entirely about shared structure . ✔
Before the example, we must earn one new piece of notation: the -> inside a requires block.
-> (compound requirement) syntax
Inside a requires-expression, a compound requirement wraps an expression in braces and optionally adds a type constraint after an arrow:
{ expr } -> Constraint;
This is read as two demands at once : (1) expr must compile , and (2) the type that expr produces must satisfy Constraint. The -> here is not a function-return arrow and not a pointer arrow — it is a special requires-only symbol meaning "…and the result type must satisfy…". Constraint must itself be a concept that takes the result type as its first argument, e.g. std::same_as<T> or std::convertible_to<int>.
+ only if a + b is exactly T.
template < typename T >
concept Addable = requires (T a, T b) {
{ a + b } -> std ::same_as < T > ; // (1) a+b compiles (2) its type is exactly T
};
Forecast: for which of these is Addable true — int, char, std::string?
std::string: s + s compiles and its type is std::string = T ⇒ {...}->same_as<T> passes ⇒ Addable<std::string> = true.
Why this step? The compound requirement has two jobs (per the definition above): the expression must compile and its type must match. Both hold.
char: c + c compiles, but its type by integer promotion is int, not char. So same_as<char> fails ⇒ Addable<char> = false.
Why this step? This is the subtle case — the expression is valid , yet the result type trips the second demand. Compound requirements catch exactly this.
int: i + i is int = T ⇒ Addable<int> = true.
Verify: Replace std::same_as<T> with the looser std::convertible_to<T>, and char now passes (its int result converts back to char). The choice of type-constraint after -> is the whole design decision. Truth table: int→true, char→false (under same_as), std::string→true. ✔
Worked example "Only accept things I can loop over AND stream each element."
A logging function must accept std::vector<int> and std::vector<std::string> but reject std::vector<NoStream> and reject a plain int (not a range).
struct NoStream {}; // has no operator<<
template < typename R >
concept Printable = requires (R r) {
std :: begin (r); std :: end (r); // (1) iterable
requires requires ( decltype ( * std :: begin (r)) x) { // (2) nested
std ::cout << x; // streamable element
};
};
template < Printable R > void log_all ( const R & r ){ for ( auto& x: r) std ::cout << x; }
The figure above shows two gates. Gate 1 (violet) asks "is it iterable?" and Gate 2 (magenta) asks "is each element streamable?". A type must pass both gates. The green path traces vector<int> sailing through both to TRUE; the magenta path traces vector<NoStream> passing Gate 1 but failing Gate 2 to FALSE.
Forecast: guess pass/fail for vector<int>, vector<NoStream>, and a bare int.
vector<int>: begin/end exist ⇒ (1) passes. Element type is int, and cout << int compiles ⇒ (2) passes ⇒ Printable = true. This is the green path in the figure.
Why this step? Both the range requirement and the element requirement are checked at the call site , before the loop is ever compiled.
vector<NoStream>: begin/end exist ⇒ (1) passes, but cout << NoStream{} does not compile ⇒ (2) fails ⇒ Printable = false. This is the magenta path in the figure.
Why this step? The nested requires catches the bad element up front — instead of a wall of errors from inside the for-loop.
bare int: std::begin(5) is ill-formed ⇒ (1) already fails ⇒ Printable = false.
Why this step? Failing (1) short-circuits; we never even test streamability.
Verify: Truth values — vector<int>: true; vector<string>: true; vector<NoStream>: false; int: false. Exactly the container/element split we wanted, all diagnosed at the call site. ✔
Worked example Empty range,
void, and a self-referential probe — do they crash the concept?
Forecast: does an empty std::vector<int>{} satisfy Printable? Does Printable<void> even compile?
Empty range std::vector<int> v{}: Printable<std::vector<int>> is a property of the type , decided at compile time — the number of elements is irrelevant. So it's true regardless of size.
Why this step? A concept never runs the loop; it only asks "does the syntax begin/end/<< type-check?" Emptiness is a runtime fact, invisible to the concept. The for loop simply iterates zero times at runtime.
Printable<void>: requires(void r) — you cannot declare a variable of type void. The requires-expression is ill-formed in its parameter list , which by the rules yields false (not a hard error) ⇒ void is silently rejected.
Why this step? This is the key degenerate rule: a malformed requirement makes the concept false, it does not blow up. Constraint checking is designed to fail gracefully.
Self-referential probe requires Printable<R>; inside Printable — this would be circular. The compiler treats an in-progress concept as not-yet-satisfied and reports a diagnostic instead of infinite recursion.
Why this step? Concepts are non-recursive by design; naming a concept inside its own definition is the one place they do error, protecting you from infinite checks.
Verify: Printable<std::vector<int>> = true (size-independent); Printable<void> = false (via ill-formed probe); a self-referential concept = ill-formed. No runtime crash occurs in any case because concepts are pure compile-time predicates. ✔
Worked example Explain the value of this and why it isn't a typo.
template < class T >
concept Sizable = requires requires (T a){ a. size (); }; // TRAP line
// and separately:
template < class T >
concept SafeNum = std ::integral < T > || requires (T a){ a. count (); };
Forecast: is requires requires a typo? For SafeNum<int>, is a.count() ever tested?
requires requires decomposed. The first requires is a requires-clause (it introduces a constraint and expects a bool). The second requires(T a){...} is a requires-expression (it builds a bool by testing that a.size() compiles). So the whole line means "constrain T to: the expression that tests a.size() is well-formed ." It is two different keywords doing two different jobs, back-to-back — legal, not a typo.
Why this step? Recognising the two grammatical roles is the entire trick; once you see clause-then-expression, the doubling stops looking wrong. Better style: collapse it to a single requires by naming the expression directly: concept Sizable = requires(T a){ a.size(); };.
When is requires requires genuinely needed? Inside another requires-expression, when you want a nested requirement that is itself an expression-test. Example from Printable (Ex. 8): requires requires(decltype(*std::begin(r)) x){ std::cout << x; }; — the outer requires (nested-requirement clause) says "this constraint must hold", and the inner requires(...) builds that constraint by testing cout << x.
Why this step? This shows the legitimate use: a nested requires needs a clause to introduce it and an expression to define it, producing the double keyword honestly.
SafeNum<int> and short-circuit. std::integral<int> = true. Concepts use short-circuit || at compile time, so the right operand requires(T a){ a.count(); } is never even instantiated . int has no .count(), but that is irrelevant — the left side already answered true. So SafeNum<int> = true.
Why this step? Short-circuit means an absent operation on the unused branch cannot break the concept.
SafeNum<std::bitset<8>>. std::integral<std::bitset<8>> = false, so the right side is now tested; bitset has .count() ⇒ true. So SafeNum<std::bitset<8>> = true.
Why this step? Confirms the right branch actually gets used when the left is false — short-circuit only skips the second operand, it does not delete it.
Verify: SafeNum<int> = true (left branch; right never checked); SafeNum<std::bitset<8>> = true (right branch); SafeNum<std::string> = false (no integral-ness, no .count()). The double requires is valid grammar in both forms shown. ✔
Recall Which cell does each example cover?
Example 1 :::> C1 satisfied
Example 2 :::> C2 one overload fails, fallback still matches
Example 3 :::> C3 all fail → hard error (no matching function)
Example 4 :::> C4 disjoint overloads, one applies
Example 5 :::> C5 subsumption, stronger wins
Example 6 :::> C6 ambiguous, neither subsumes
Example 7 :::> C7 compound requirement / result type
Example 8 :::> C8 nested concept, real container (word problem)
Example 9 :::> C9 degenerate: empty range, void, self-reference
Example 10 :::> C10 exam twist: requires requires + || short-circuit
Mnemonic The five fates of a constraint
PASS · DROP · DIE · RANK · TIE — a type either Pass es, gets silently Drop ped (but a fallback catches it), everyone drops so the call Die s (no match), two survivors get Rank ed by subsumption, or two survivors Tie (ambiguous error).
Parent: Concepts (C++20) — constraining templates · Prereqs: Templates — function & class templates , SFINAE and std::enable_if , type_traits — std::integral, std::floating_point , Overload Resolution & Subsumption , constexpr — compile-time evaluation , Ranges library (C++20) , auto and decltype .