Visual walkthrough — Concepts (C++20) — constraining templates
This page builds on Concepts (C++20) — constraining templates and its prerequisites: Templates — function & class templates, SFINAE and std::enable_if, type_traits — std::integral, std::floating_point, and Overload Resolution & Subsumption.
Step 1 — What a template really is: a machine with a blank slot
WHAT. Think of template<typename T> T sq(T x){ return x*x; } as a rubber stamp with a hole where the type goes.
WHY. We want one recipe that works for int, double, float, ... instead of writing the same function ten times. The blank slot T is the whole point.
PICTURE. In the figure, the stamp on the left has an empty grey socket labelled T. A type (int) drops into the socket, and the stamp presses out a concrete function int sq(int x).

The danger: the stamp will accept any type dropped into the socket — even a std::string, which has no meaningful *. Nothing stops the bad type at the door. That missing door is the problem concepts solve.
Step 2 — The old failure: the type gets in, then explodes inside
WHAT. The type is admitted first; the check happens later, deep in the body.
WHY it hurts. The compiler's finger points at return x*x; — a line inside the recipe — not at your call sq(myString). This is the "200-line template error" the parent note warned about.
PICTURE. The string slips past the open socket (green arrow), gets stamped, and a red explosion erupts at x*x far from the call site. The error's location (red star) is nowhere near where you made the mistake (grey call site).

We want to move that red star back to the front door. That is Step 3.
Step 3 — A concept is a bouncer: a compile-time yes/no on the type
WHAT. A concept is always introduced under its own template head, so the placeholder type has a name before we use it:
The first line declares T; only after that may T appear in the body. The || is plain "OR": pass if either holds.
WHY this tool and not an if? An if runs at runtime, when the type is long gone — the type only exists during compilation. We need a check that lives at compile time. std::integral and std::floating_point (from type_traits — std::integral, std::floating_point) are exactly such compile-time questions, so we build our concept out of them.
PICTURE. The socket now has a bouncer. int walks up, the bouncer evaluates Numeric<int> = true, opens the rope. std::string walks up, Numeric<string> = false, the rope stays shut and the string is turned away at the door — no explosion inside.

Step 4 — Attaching the bouncer: four doorways, one guard
WHAT. All four below stamp out the same guarded function sq:
- Abbreviated parameter — the guard rides on the parameter itself:
- Constrained template parameter — the guard replaces
typenamein the type list: - Leading requires-clause — the guard is a separate clause after the template head:
- Trailing requires-clause — the guard sits after the parameter list, before the body:
WHY four? Ergonomics. Form 1 is shortest for one-liners; form 2 reads cleanest with a single named concept; forms 3 and 4 shine when the condition is a complicated ad-hoc boolean not worth naming. Same bouncer, four different doorframes.
PICTURE. Four doorframes drawn side by side, each with the same bouncer figure standing in it, each admitting int and rejecting string. The point of the picture: swapping doorframes never changes who gets in.

Step 5 — Building a bouncer by hand: the requires-expression
WHAT. To make "types you can add, whose sum is still a T":
T a, T bare fictional — never actually constructed. They exist only so we can writea+band ask the compiler "would this line compile?"{ a + b }is a compound requirement: the expression must be valid.-> std::same_as<T>uses the concept just defined to pin the type of the result to exactlyT.
WHY a requires-expression and not a normal function? A normal function would try to run a+b and compute a number. We do not want a number — we want the yes/no "does this even compile?". The requires-expression is the only tool that returns compilability itself as a bool.
PICTURE. A checklist clipboard. Each requirement line has a checkbox. The bouncer ticks a box only if that line compiles. All boxes ticked → the whole expression is true (green). One box fails → false (red). Nothing on the clipboard is ever executed.

Step 6 — Overloading: two bouncers, and the silent turn-away
WHAT. Given
the call describe(5) (an int):
- (A)'s bouncer:
integral<int> = true→ stays in the candidate set. - (B)'s bouncer:
floating_point<int> = false→ removed, quietly.
One candidate survives → it is chosen. This is Overload Resolution & Subsumption in action, no SFINAE and std::enable_if gymnastics required.
PICTURE. Two doors A and B. The value 5 approaches; door A lights green and opens, door B greys out and closes (removed, not an alarm). Below, the value 3.14 does the mirror: B opens, A greys out.

Step 7 — Edge & degenerate cases: the corners you must never hit
Case α — a type satisfies BOTH bouncers, and one subsumes the other.
Suppose SignedIntegral<T> is defined as Integral<T> and more. Then SignedIntegral subsumes Integral: it demands strictly more. If a type passes both, the compiler prefers the more constrained door. This is why concepts give ordered specialisation.
Case β — both pass but NEITHER subsumes the other (ambiguity).
Now imagine two overloads guarded by unrelated concepts, say Hashable<T> and Serializable<T>, and a type T that satisfies both. Neither concept requires strictly more than the other — they are incomparable, like two overlapping circles with no nesting. The compiler cannot pick a "more constrained" winner, so the call is ambiguous and you get a hard "call to ... is ambiguous" error. The fix: add a third overload constrained by Hashable<T> && Serializable<T> (which subsumes both), or make one requirement strictly imply the other.
Case γ — a type satisfies NEITHER bouncer.
describe("hi") with a const char*: both integral and floating_point are false. Both candidates removed → zero survivors → the compiler reports the clean message "no matching function for call to describe" — pointed at your call site, exactly the win we wanted.
Case δ — the empty/degenerate requires-expression.
A requires-expression with no requirements, requires{ }, is vacuously true (an empty conjunction). And a fictional variable of a type with a deleted operator makes that one line fail — turning the whole expression false — without ever touching other lines.
PICTURE. Four mini-panels: (α) two nested rings, the inner "SignedIntegral" ring highlighted as the winner; (β) two overlapping but un-nested circles with a red "ambiguous" mark in the shared middle; (γ) both doors closed with a single clean signpost "no matching function"; (δ) an empty checklist glowing green (vacuously true) beside a checklist with one red box dragging the whole result to red.

The one-picture summary
Everything compresses to one pipeline: a type meets a compile-time bouncer before the body is stamped; passing types flow to a clean stamped function, failing types are turned away at the door with a call-site message; when several doors apply, the most constrained wins — unless none of them is more constrained than another, in which case the call is ambiguous.

Recall Feynman: the whole walkthrough in plain words
A template is a rubber stamp with an empty socket — drop a type in, out comes real code. Old C++ let any type into the socket and only screamed when the stamped code tried to do something the type couldn't (like multiplying a string) — and it screamed from inside the recipe, far from your mistake. A concept is a bouncer we put at the socket: a compile-time yes/no question about the type, like "is it a number?" We build the bouncer either from ready-made questions (std::integral || std::floating_point) or by hand with a requires-expression — a clipboard of lines the compiler tries to compile (never run); all lines compile → true, one fails → false. You can attach this bouncer four different ways; they all mean the same thing. When you have several functions each with its own bouncer, a call is shown to all of them; the ones whose bouncer says "no" are quietly removed (that is not an error), and if two say "yes" the pickier one wins — but if neither is pickier, the call is ambiguous and fails. Only when nobody is left do you get a "no matching function" error — and it's the clean one, pointing right at your call.
Recall Quick self-test
In each line below, the ::: marker separates a prompt (left) from its answer (right); read the left side, try to answer, then check the right.
A concept is a compile-time predicate on ::: types (answering true/false while compiling, never at runtime).
{ a + b } -> std::same_as<T> tests ::: that a+b compiles AND its type is exactly T.
An unsatisfied constraint causes ::: silent removal of the candidate, not an error (error only if none remain).
When two constrained overloads both apply and one subsumes the other, the winner is ::: the more-constrained one (subsumption).
When two constrained overloads both apply but neither subsumes the other, the result is ::: an ambiguity error ("call is ambiguous").
A requires-expression with no requirements evaluates to ::: true (vacuously — empty conjunction).