Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
__cplusplus              = 202002
__cpp_concepts           = 202002
__cpp_lib_concepts       = 202002
__cpp_lib_ranges         = 202110
__cpp_consteval          = 202211

Apple 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::list<int> l{3,1,2};
std::sort(l.begin(), l.end());
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.cpp

138 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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. abbreviated

Form 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::integral<int>                              = 1
std::random_access_iterator<vector::iterator>   = 1
std::random_access_iterator<list::iterator>     = 0

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::sortable<list::iterator> = 1   (!)
std::random_access_iterator<list::iterator> = 0

std::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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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());
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_if or a trailing decltype (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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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; });
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ranges filter|transform  = 4 16 36

One 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::ranges::sort(txns, {}, &Txn::addr);      // sort by a member

That 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

techniquerequirement is inerrorslinesfirst error inrejects before instantiationselects overloads
unconstrained templatethe body4 (measured)138 (measured)a library headernono
a commentnowhere checkable4138a library headernono
static_assertthe body2 (measured)57 (measured)my fileno (measured)no
SFINAE / enable_ifthe signature, unreadablyvariesoften worsevariesyesyes
tag dispatchan extra parametern/an/an/ayesyes
a conceptthe signature1 (measured)16 (measured)my fileyes (measured)yes
std::ranges:: algorithmthe library's signature1 (measured)22 (measured)my fileyesyes

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::sortable accepts a list iterator.
  • static_assert a 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 a requires clause for compound ones.
  • Use constraints to select overloads instead of tag dispatch or SFINAE.
  • Use if constexpr when 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 consteval are all available on this compiler, checked before anything was built on them.
  • That an unconstrained std::sort on a list gives 4 errors across 138 lines, the first inside make_heap.h.
  • That the concept version gives 1 error in 16 lines, at the call site, naming random_access_iterator in English.
  • That std::ranges::sort gives 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_assert gives 2 errors, because the body still instantiates, and cannot select overloads.
  • That std::sortable accepts 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_if Chapter 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::sort on a std::list gives 4 errors across 138 diagnostic lines, the first inside make_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::sort gives 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_assert gives 2 errors, because the assert fires and the body instantiates anyway — and it cannot select overloads.
  • Measured trap: std::sortable<list::iterator> is 1 while random_access_iterator is 0, and ranges::sort requires both. A concept's name is not its definition.
  • Constraints participate in overload resolution, replacing tag dispatch and SFINAE; if constexpr remains right for one function with two branches.
  • Ranges views compose lazily — measured filter | transform giving 4 16 36 in one pass — which supplies the transform_if Chapter 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.