C++ Part I · Module 21 · Modern C++ Type and Utility Tools
Concepts and Constraining a Template
Module 18 established that a template's requirements on its arguments are real, consequential and written nowhere the compiler can check — documented in a comment at best, and discovered by the reader of a diagnostic at worst. Module 20 then measured the consequence at its sharpest: calling std::sort on a std::list produces four errors spread across a hundred and thirty-eight diagnostic lines, the first of them reported from inside a library header the programmer has never opened, about an expression they did not write, naming not one line of their own code except the call that started it. The requirement being violated is simple and sayable in six words — sort needs a random-access iterator — and the compiler says it in none of those lines. Concepts, added in C++20, are the fix: a named, checkable predicate on types that goes in the signature, so the requirement is part of the interface rather than an emergent property of the body. This chapter closes that loop with a measurement. The same mistake is expressed five ways — unconstrained library call, constrained library call, unconstrained template of my own, the same with a concept, and the same with the pre-C++20 static_assert technique — and the diagnostic is measured each time. The concept version is sixteen lines, reports at the call site, and states the unsatisfied requirement in English. It also does something static_assert cannot, which the chapter measures separately.
Advanced20 min readC++Part IFundamentalsModern C++
Part I · Module 21 · Chapter 21.8 · Concepts and Constraining a Template
1. The Toolchain, Checked First
Concepts are C++20, and a chapter built on a feature the compiler does not have would be worthless. So before anything else:
__cplusplus = 202002
__cpp_concepts = 202002
__cpp_lib_concepts = 202002
__cpp_lib_ranges = 202110
__cpp_consteval = 202211Apple clang 21.0.0 on arm64, with -std=c++20. Language concepts, the library concepts in <concepts>, ranges, and consteval are all present; -std=c++23 also compiles clean. Every measurement below is from this compiler, and every claim about a diagnostic is a diagnostic I read.
That check matters because this curriculum has had to label things unrunnable before, and the honest thing is to establish the capability rather than assume it.
2. The Problem, Restated
Chapter 20.2 §7 measured this and it is the baseline for everything here:
std::list<int> l{3,1,2};
std::sort(l.begin(), l.end());/usr/include/c++/v1/__algorithm/make_heap.h:35:34: error: invalid operands to
binary expression ('std::__list_iterator<int, void *>' and
'std::__list_iterator<int, void *>')Four errors, 138 diagnostic lines, first reported inside make_heap.h. The violated requirement is that std::sort needs a random-access iterator and a std::list supplies a bidirectional one. That sentence does not appear anywhere in the output. What appears is the symptom — it - it is not defined for a list iterator — discovered deep inside an instantiation and reported where it was discovered.
The cause is structural, and Chapter 18.6 §5 named it: an unconstrained template's requirements are whatever its body happens to use, so they cannot be checked until the body is instantiated, so the error is reported from the body. The requirement lived in the implementation rather than the interface.
3. The Measurement
The same mistake, five ways.
CASE STD ERRORS LINES FIRST ERROR IN
unconstrained std::sort c++17 4 138 make_heap.h
std::ranges::sort c++20 1 22 diag_b.cpp
my template, unconstrained c++20 1 52 diag_c.cpp
my template + concept c++20 1 16 diag_d.cpp
my template + static_assert c++17 2 57 diag_e.cpp138 lines to 16 — a factor of 8.6 — and the first error moves out of a library header and into my own file.
The concept version's output in full is short enough to read:
diag_d.cpp:5:39: error: no matching function for call to 'mySort'
5 | int main() { std::list<int> l{3,1,2}; mySort(l.begin(), l.end()); return 0; }
| ^~~~~~
diag_d.cpp:4:6: note: candidate template ignored: constraints not satisfied
[with It = iterator]
diag_d.cpp:3:11: note: because 'std::__list_iterator<int, void *>' does not
satisfy 'random_access_iterator'
.../__iterator/concepts.h:222:36: note: because 'derived_from<_ITER_CONCEPT<...>,
random_access_iterator_tag>' evaluated to false
.../__concepts/derived_from.h:27:24: note: because
'is_base_of_v<std::random_access_iterator_tag, std::bidirectional_iterator_tag>'
evaluated to false
1 error generated.Three things are different in kind, not just in length.
The error is at my call site, line 5 of my file, not line 35 of a header I have never read.
The note states the requirement in English: "because std::__list_iterator<int, void*> does not satisfy random_access_iterator". That is the six-word sentence §2 said was missing.
The remaining notes are a causal chain, each explaining the previous one, ending at the primitive fact — is_base_of_v<random_access_iterator_tag, bidirectional_iterator_tag> is false. You can stop reading at whatever depth answers your question.
std::ranges::sort gives 22 lines with the same shape, because the standard library's ranges algorithms are constrained in exactly this way. That is the practical headline for most readers: you get this improvement for free by calling std::ranges::sort instead of std::sort.
4. Writing One
The declaration form:
template <typename T>
concept Addable = requires(T a, T b) { a + b; };A concept is a named compile-time predicate on types. The requires expression lists expressions that must be valid, not expressions that must be true — a + b; means "a + b must compile", and nothing is evaluated.
Then four interchangeable ways to apply it:
template <Addable T> T twice(T x) { return x + x; } // 1. as the parameter kind
template <typename T> requires Addable<T> T twice(T x); // 2. a requires-clause
template <typename T> T twice(T x) requires Addable<T>; // 3. trailing
auto twice(Addable auto x) { return x + x; } // 4. abbreviatedForm 1 is the most readable for a single constraint and is what §3 measured. Form 2 is necessary for compound conditions. Form 4 is shortest and hides the template entirely.
A requires expression can state four kinds of requirement:
template <typename T>
concept Register = requires(T r, std::uint32_t v) {
r.read(); // simple: must compile
{ r.width() } -> std::convertible_to<unsigned>; // compound: and the type
typename T::value_type; // type: must name a type
requires std::is_trivially_copyable_v<T>; // nested: must be true
};The compound form with -> is the one worth learning, because it constrains the result and not just the validity — which is how you avoid accepting a width() that returns a std::string.
5. The Library's Concepts
You will usually not write one. <concepts> and <iterator> supply the vocabulary:
std::integral<int> = 1
std::random_access_iterator<vector::iterator> = 1
std::random_access_iterator<list::iterator> = 0The core set: std::integral, std::floating_point, std::same_as, std::convertible_to, std::derived_from, std::constructible_from, std::copyable, std::movable, std::equality_comparable, std::totally_ordered, std::invocable, std::predicate, std::regular.
The iterator set formalises Chapter 20.2 §1's categories exactly: std::input_iterator, std::forward_iterator, std::bidirectional_iterator, std::random_access_iterator, std::contiguous_iterator — and the range equivalents std::ranges::input_range and so on.
Prefer the library's concepts to your own. They are correct, they compose, and a diagnostic naming std::random_access_iterator is immediately recognisable where one naming MyIterRequirements is not.
6. Pick the Right Concept, Not the Matching Name
This is the trap, and it is worth its own section because the name is actively misleading.
std::sortable<list::iterator> = 1 (!)
std::random_access_iterator<list::iterator> = 0std::sortable is satisfied by a list iterator. It is defined as permutable plus indirect_strict_weak_order, and a bidirectional iterator meets both — you genuinely can permute a list and compare its elements. What std::sortable does not require is random access, which std::ranges::sort needs in addition:
template <random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R> sort(R&& r, Comp = {}, Proj = {});Two constraints, random_access_range and sortable. Constraining your own sort with std::sortable alone would accept a list and then fail in the body — reproducing §2's problem with a concept attached, which is the worst of both.
The lesson generalises: a concept's name tells you what it is called, not what it requires. Read the definition, or check it with a static_assert against a type you expect to fail, which is Exercise 2. This is the same discipline as Chapter 20.5 §6's precondition — a requirement you have not verified is a requirement you have assumed.
7. A Concept Rejects Before Instantiation
This is the property static_assert cannot replicate, and it is the strongest technical argument in the chapter.
Chapter 18.10 §6 recommended static_assert as the pre-C++20 way to improve a template's diagnostic, and it does help — §3 measured 57 lines against the unconstrained 52, with the assert firing and naming my message. But look at the error count: 2 errors, not 1.
diag_e.cpp:7:19: error: static assertion failed due to requirement
'std::is_base_of_v<std::random_access_iterator_tag,
std::bidirectional_iterator_tag>': mySort requires a random-access iterator
(vector/array/deque), not a list or map
diag_e.cpp:10:51: error: invalid operands to binary expression
('std::__list_iterator<int, void *>' and 'int')The assert fires with my message — good — and then compilation continues into the body, which fails on its own terms and produces a second, confusing error. The reader now has to work out that the second error is a consequence of the first.
A concept does not have this problem, because constraint checking happens during overload resolution, before the body is instantiated at all. The proof is a constrained template whose body is deliberate nonsense:
template <std::random_access_iterator It>
void mySort(It b, It e) { b.this_member_does_not_exist(); (void)e; }
std::list<int> l{3,1,2};
mySort(l.begin(), l.end());errors: 1 lines: 16
diag_f.cpp:7:39: error: no matching function for call to 'mySort'
diag_f.cpp:5:11: note: because 'std::__list_iterator<int, void *>' does not
satisfy 'random_access_iterator'
-> the body's nonsense was never reported: the constraint rejected the call first.One error, 16 lines, and this_member_does_not_exist is never mentioned. The template was removed from the candidate set before its body was looked at.
That has two consequences beyond diagnostics. A constrained template participates in overload resolution by its constraints, so you can provide two implementations and let the compiler pick — §8. And a static_assert can only fire inside a function that was already selected, so it cannot do that at all.
8. Constraints Choose Overloads
Because constraints are checked during overload resolution, they can select between implementations. This replaces tag dispatch and SFINAE with something readable:
template <std::random_access_iterator It>
void advanceBy(It& it, std::ptrdiff_t n) { it += n; } // O(1)
template <std::bidirectional_iterator It>
requires (!std::random_access_iterator<It>)
void advanceBy(It& it, std::ptrdiff_t n) { while (n-- > 0) ++it; } // O(n)The more-constrained overload wins when both are viable, by the partial ordering of constraints, so for a vector iterator the first is chosen without the explicit negation — though writing the negation makes the intent obvious and avoids relying on subsumption rules that are easy to get wrong.
Compare the three generations of this technique:
- Tag dispatch (Chapter 20.2 §8): an extra overload taking a tag, plus a forwarding function. Works everywhere, verbose.
- SFINAE with
std::enable_ifor a trailingdecltype(Chapter 21.3 §4): one function, unreadable signature, diagnostics that are worse than the unconstrained version. - Concepts: the requirement in the signature, in English, selecting overloads.
And the if constexpr alternative from Chapter 18.8 §9 is still right when there is one function with two branches rather than two functions — the difference being that if constexpr chooses code inside a function that was already selected, while a constraint chooses the function.
9. Ranges, Which Are Built On This
Concepts are what made the ranges library possible, and it closes a gap Chapter 20.7 §6 had to leave open — the absence of a transform_if:
std::vector<int> v{1,2,3,4,5,6};
auto evensSquared = v | std::views::filter([](int x){ return x%2==0; })
| std::views::transform([](int x){ return x*x; });ranges filter|transform = 4 16 36One pass, no intermediate container, no allocation — the views compose into a single iteration rather than materialising a temporary. Chapter 20.7 §6 listed three responses to the missing transform_if and named this as the correct one where available; it is available here.
The ranges algorithms also take a container directly rather than two iterators, and accept a projection:
std::ranges::sort(txns, {}, &Txn::addr); // sort by a memberThat is Chapter 20.6 §7's comparator, replaced by naming the member. Both are improvements that exist because the algorithms are constrained well enough to know what they are being given.
10. What It Costs
C++20. The hard requirement. Pre-C++20 the best available is Chapter 18.10 §6's static_assert, which §7 measured as a real improvement with a real limitation.
Nothing at run time. Constraints are checked at compile time and vanish. There is no object, no indirection, no check in the generated code — the same situation as Chapter 21.1's alias.
Build time. Constraint checking is work, and a heavily-constrained template library measurably costs compile time. In exchange, a failed build fails much faster, because the error surfaces at overload resolution rather than after instantiating a page of templates.
Learning the vocabulary. The library's concept set is large and the names are only approximately self-describing — §6's std::sortable is the cautionary example.
And a design cost worth naming: a constraint is an interface promise. Once published, loosening it is a compatible change and tightening it breaks callers. Constraining to exactly what the body needs — rather than to what seems tidy — is the discipline, and it is Chapter 17.9 §3's narrow-interface argument applied to templates.
11. The Comparison
| technique | requirement is in | errors | lines | first error in | rejects before instantiation | selects overloads |
|---|---|---|---|---|---|---|
| unconstrained template | the body | 4 (measured) | 138 (measured) | a library header | no | no |
| a comment | nowhere checkable | 4 | 138 | a library header | no | no |
static_assert | the body | 2 (measured) | 57 (measured) | my file | no (measured) | no |
SFINAE / enable_if | the signature, unreadably | varies | often worse | varies | yes | yes |
| tag dispatch | an extra parameter | n/a | n/a | n/a | yes | yes |
| a concept | the signature | 1 (measured) | 16 (measured) | my file | yes (measured) | yes |
std::ranges:: algorithm | the library's signature | 1 (measured) | 22 (measured) | my file | yes | yes |
12. What to Actually Do
- Call
std::ranges::algorithms instead of the iterator-pair forms. Measured: 22 lines instead of 138, for a one-word change. - Constrain every template you write that has a non-obvious requirement.
- Use the library's concepts, not hand-rolled ones — they are correct, composable and recognisable in a diagnostic.
- Read a concept's definition before relying on its name. Measured:
std::sortableaccepts a list iterator. static_asserta concept against a type you expect to fail, to check you picked the right one.- Constrain to what the body needs, not to what looks tidy. A published constraint is an interface promise.
- Prefer
template <Concept T>for one constraint and arequiresclause for compound ones. - Use constraints to select overloads instead of tag dispatch or SFINAE.
- Use
if constexprwhen it is one function with two branches, and a constraint when it is two functions. - Before C++20, use
static_assert— and know it gives a second error because the body still instantiates.
13. Mistakes Worth Making Once
Constraining with the concept whose name matches the algorithm. Measured: std::sortable is satisfied by a list iterator, and ranges::sort needs random_access_range as well.
Relying on static_assert alone and being surprised by the second error. Measured: 2 errors, because the body still instantiates.
Writing your own Iterable concept when std::ranges::range exists and is right.
Over-constraining. A template requiring std::regular when it only needs std::movable rejects types that would have worked, and tightening later breaks callers.
Putting the requirement in a comment. Measured cost: 138 lines and a library header.
Forgetting that requires(T a) { a + b; } checks validity, not truth. For a truth condition you need the nested form, requires expr;.
Expecting a constraint to cost something at run time. It does not exist at run time.
Assuming the subsumption rules will pick the overload you meant. Write the negation explicitly when two constraints overlap.
14. Interview Relevance
The frozen curriculum marks this chapter High, and it is a good senior question because the answer reveals whether someone has felt the problem.
"What problem do concepts solve?" Requirements that live in a template's body rather than its interface, so violations are reported from inside the implementation. The strong answer gives the measurement: 4 errors across 138 lines from a library header, against 1 error in 16 lines at the call site naming the requirement.
"How is a concept better than a static_assert?" Two ways. The diagnostic is one error rather than two, because a constraint rejects the call before the body instantiates — demonstrable with a constrained template whose body is nonsense. And constraints participate in overload resolution, which an assert cannot.
"Write a concept." template <typename T> concept Addable = requires(T a, T b) { a + b; }; — and the detail that the expressions must be valid, not true.
"What is the easiest way to get better template errors today?" Call std::ranges:: algorithms. Measured 22 lines instead of 138, with no code of your own changed.
"Any traps?" std::sortable is satisfied by a list iterator; a concept's name is not its definition.
"Do constraints cost anything?" Not at run time. Build time, yes — and a failing build fails faster.
15. What You Should Be Able to Say Now
- That C++20 concepts, library concepts, ranges and
constevalare all available on this compiler, checked before anything was built on them. - That an unconstrained
std::sorton a list gives 4 errors across 138 lines, the first insidemake_heap.h. - That the concept version gives 1 error in 16 lines, at the call site, naming
random_access_iteratorin English. - That
std::ranges::sortgives 22 lines for a one-word change. - That a concept rejects before instantiation — proven by a constrained template whose nonsense body was never reported.
- That
static_assertgives 2 errors, because the body still instantiates, and cannot select overloads. - That
std::sortableaccepts a list iterator, so a concept's name is not its definition. - That constraints participate in overload resolution and replace tag dispatch and SFINAE.
- That ranges views compose lazily, which supplies the
transform_ifChapter 20.7 §6 lacked.
16. Exercises
Exercise 1 — the table. Reproduce §3's five cases on your compiler and record errors, lines and the file of the first error. Report any differences from the figures here.
Exercise 2 — check the concept. static_assert ten library concepts against std::list<int>::iterator, std::vector<int>::iterator and int*, predicting each first. Find at least one result that surprises you.
Exercise 3 — before instantiation. Reproduce §7's constrained template with a nonsense body. Then remove the constraint and compare.
Exercise 4 — write three. Write concepts for: a type with a read() returning something convertible to uint32_t; a type usable as a map key; and a trivially copyable type of at most 8 bytes. Test each against three types.
Exercise 5 — overload selection. Implement §8's advanceBy with constraints, verify the right overload is chosen for a vector and a list with a counting iterator, then remove the explicit negation and see whether it still works.
Exercise 6 — the three generations. Implement one constrained function three ways — tag dispatch, enable_if SFINAE, and a concept — and compare the signatures, the diagnostics on misuse, and the lines of code.
Exercise 7 — ranges. Rewrite Chapter 20.10's filter-sort-summarise pipeline with ranges views and std::ranges:: algorithms. Count the intermediate containers before and after.
Exercise 8 — over-constraining. Constrain a function with std::regular, find a type that works but is rejected, and relax the constraint to exactly what the body needs.
17. Summary
- Toolchain checked first:
__cpp_concepts = 202002,__cpp_lib_ranges = 202110, C++23 compiles. Every figure below is from this compiler. - Measured baseline: unconstrained
std::sorton astd::listgives 4 errors across 138 diagnostic lines, the first insidemake_heap.h, naming no line of mine but the call. - Measured: the concept-constrained version gives 1 error in 16 lines, at my call site, with the note "does not satisfy
random_access_iterator" — an 8.6× reduction, and the error moved out of a library header. - Measured:
std::ranges::sortgives 22 lines with the same shape, for a one-word change at the call site. - Measured: a concept rejects the call BEFORE instantiating the body — a constrained template whose body called a nonexistent member still gave 1 error in 16 lines and never mentioned it.
- Measured:
static_assertgives 2 errors, because the assert fires and the body instantiates anyway — and it cannot select overloads. - Measured trap:
std::sortable<list::iterator>is 1 whilerandom_access_iteratoris 0, andranges::sortrequires both. A concept's name is not its definition. - Constraints participate in overload resolution, replacing tag dispatch and SFINAE;
if constexprremains right for one function with two branches. - Ranges views compose lazily — measured
filter | transformgiving 4 16 36 in one pass — which supplies thetransform_ifChapter 20.7 §6 lacked. - No run-time cost; the costs are C++20, build time, the vocabulary, and the fact that a published constraint is an interface promise.
That completes Module 21 and, with it, the type-and-utility toolkit. Eight chapters have each removed a design problem rather than added a feature: a name for a type, a value computed before the program runs, a way to ask what an expression produces, a way to carry several values or none or one of a choice, a way to make two quantities that share a representation refuse to be confused, and finally a way for a template to say what it needs. The thread through all eight is the module's frozen description — tools that let a type say more about itself — and the recurring measurement has been that the tool which says more is almost never the one that costs more.
Module 22 is Error Handling and Diagnostics, and its frozen blurb describes failure as a design input. It is the natural successor to this module because two chapters here deliberately stopped short of it: Chapter 21.5 §8 noted that std::optional reports absence and says nothing about why, and Chapter 21.6 §7 noted that a failure carrying its own payload wants a variant or a result type rather than an empty state. Module 22 takes that question up properly — exceptions explained including the unwinding that makes RAII the only reliable cleanup mechanism, the honest alternatives to them, and the assertions and invariants that let a long-running model fail loudly instead of quietly producing wrong answers, which is a theme this curriculum has now measured failing in nine separate ways.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
