Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 9 – Templates and concepts

C++20 concepts and requires-clauses

Constrain C++ templates with standard concepts, write your own with requires-expressions, and compare the errors two compilers print with and without them.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write a template constrained by a concept in each of the four forms C++20 allows
  • Read a constrained error from your own call down to the missing operation
  • Define a concept with simple, type, compound and nested requirements
  • Predict which overload the compiler calls when two constraints overlap
  • Distinguish a type that satisfies a concept from one that models it

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a template constrained by a concept in each of the four forms C++20 allows
  • Read a constrained error from your own call down to the missing operation
  • Define a concept with simple, type, compound and nested requirements
  • Predict which overload the compiler calls when two constraints overlap
  • Distinguish a type that satisfies a concept from one that models it

Before you start

On this page

Call an unconstrained median() on a vector of readings that have no comparison operators, and Clang prints 334 lines of errors, every one inside the helpers of std::sort; GCC 16 prints 1,457. Write std::totally_ordered where the template said typename, and both compilers stop at your own call with one error that names the missing ==. That word is a concept: A named compile-time test of template arguments, such as std::integral<T>, defined as template <typename T> concept Name = condition;. Used in a template's declaration, it makes the template accept only the types that satisfy it, and a type that fails is reported at the call.: a named requirement on a template argument, checked before the template is used.

You will constrain templates in the four places C++20 allows, read the short error a constraint gives, write concepts of your own from requirements, predict which overload wins when two constraints overlap, and tell a type that merely compiles with a concept from one that meets its meaning. Every example ran with Apple clang 21 and GCC 16.

A constraint can go in four places, all meaning the same

std::integral<T> asks whether T is an integer type, and the compiler answers it with true or false while it compiles. Written in a template’s declaration, as in template <std::integral T> T gcd(T a, T b), it lets gcd accept int and long and refuse double at the call. Concepts arrived in C++20 and do three things a plain template cannot: they state what a template needs in its declaration instead of hiding it in the body, they reject a wrong type at the call with a message that names the missing requirement, and they let you overload functions on what a type can do.

The program below defines one concept of its own, Number, as “an integer type or a floating-point type”, and then uses it in each of the four places a constraint can go:

average.cpp C++ · average.cpp
// Four ways to write "T must be a number".
#include <concepts>
#include <print>
#include <string>
#include <vector>

// A concept: a named question about a type that the compiler answers with true or false.
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;

// 1. The concept in place of typename.
template <Number T>
double average(const std::vector<T>& values) {
    T sum{};
    for (const T& v : values) sum += v;
    return values.empty() ? 0.0 : sum / static_cast<double>(values.size());
}

// 2. A requires-clause after the template parameter list.
template <typename T>
    requires Number<T>
T largest(const std::vector<T>& values) {
    T best = values.front();
    for (const T& v : values)
        if (v > best) best = v;
    return best;
}

// 3. A trailing requires-clause, after the function's parameter list.
template <typename T>
T at_least_zero(T value) requires Number<T> {
    return value < 0 ? T{} : value;
}

// 4. A constrained auto parameter: an abbreviated function template.
double half(Number auto x) { return x / 2.0; }

// bool is an integral type, so a concept that must refuse it says so, here built on Number.
template <typename T>
concept NumberNotBool = Number<T> && !std::same_as<T, bool>;

int main() {
    std::vector<int> marks{72, 85, 91};
    std::vector<double> temps{31.5, 29.0, 33.25};
    std::println("average: {:.2f} and {:.2f}", average(marks), average(temps));
    std::println("largest: {} and {}", largest(marks), largest(temps));
    std::println("at_least_zero(-4) = {}, half(7) = {}", at_least_zero(-4), half(7));
    std::println("Number<std::string> = {}", Number<std::string>);
    std::println("Number<bool> = {}, Number<char> = {}", Number<bool>, Number<char>);
    std::println("NumberNotBool<bool> = {}, NumberNotBool<int> = {}", NumberNotBool<bool>, NumberNotBool<int>);
}
One concept, four forms, both compilers Python · average_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra average.cpp -o average && ./average
g++-16 -std=c++23 -Wall -Wextra average.cpp -o average-gcc && ./average-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra average.cpp -o average && ./average
average: 82.67 and 31.25
largest: 91 and 33.25
at_least_zero(-4) = 0, half(7) = 3.5
Number<std::string> = false
Number<bool> = true, Number<char> = true
NumberNotBool<bool> = false, NumberNotBool<int> = true
$ g++-16 -std=c++23 -Wall -Wextra average.cpp -o average-gcc && ./average-gcc
average: 82.67 and 31.25
largest: 91 and 33.25
at_least_zero(-4) = 0, half(7) = 3.5
Number<std::string> = false
Number<bool> = true, Number<char> = true
NumberNotBool<bool> = false, NumberNotBool<int> = true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 average_session.py

The four forms mean the same thing to the compiler:

  1. The concept in place of typename: template <Number T>. This is the shortest form when a constraint names one concept of one parameter.
  2. A requires-clause: The word requires and a condition attached to a template, as in template <typename T> requires std::integral<T>, or after a function's parameters. The template takes part in overload resolution only for arguments that satisfy the condition. after the template parameter list: template <typename T> requires Number<T>. Concepts and constant boolean expressions may follow requires, joined with && and ||, which suits conditions on several parameters. A negation, a comparison or a call goes in parentheses there: requires Number<T> && (!std::same_as<T, bool>).
  3. A trailing requires-clause after the function’s parameters. It is the form for a member function of a class template, whose parameter was introduced by the class: void print() const requires Printable<T>; makes print exist only for element types that can be printed.
  4. A constrained auto parameter: double half(Number auto x). Like the abbreviated function template: A function with auto in the type of a parameter, such as void show(const auto& x) (C++20). Each auto invents a template parameter of its own, so the function is a function template written without a template line. More in “C++ function templates and argument deduction” of the function templates lesson, each Number auto invents a template parameter of its own, now with the constraint attached (paper P1141R2).

A concept is also an expression of type bool that the compiler evaluates: the last three lines print Number<std::string> and four more checks. Number<bool> and Number<char> are both true, because the standard counts bool, char and the other character types among the integral types. If an average of bool values should be refused, a concept has to say so: NumberNotBool, built as Number<T> && !std::same_as<T, bool>, is false for bool. Writing the test into Number itself needs parentheses, (std::integral<T> || std::floating_point<T>) && !std::same_as<T, bool>, because && binds more tightly than ||. A concept checks exactly what you wrote, so decide which types belong before you name it.

Which form to write depends on what the condition says and where it goes. Answer the three questions for a template of your own:

Interactive · answer and compare Which of the four forms should a constraint take? Answer the questions about the template you are constraining. A form that cannot say what you need is ruled out, with the reason.
Must the body name the type, for example to declare a variable of type T?
Does the condition need more than one concept for a parameter, such as A<T> || B<T>?
Is it a member function that constrains the T of its class template?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

The four forms of a constraint against naming the type, joining conditions, reaching a class's T and length
Form Names the type Joins conditions Reaches the class's T Short to write
template <Number T> Good T is declared, so average can write T sum{} Poor one concept for each parameter: no || between two concepts Poor constrains a parameter of its own template line, not the class T Good one word in place of typename
A requires-clause Good T is declared as usual: largest reads T best Good concepts and traits joined with && and ||, such as Number<T> && (!std::same_as<T, bool>) Poor follows a template parameter list, which a plain member function lacks Mixed one more line than the concept in place
A trailing requires-clause Good T is declared before the function, as at_least_zero shows Good the same conditions as a requires-clause, after the parameters Good void print() const requires Printable<T>; constrains the class T Mixed the condition comes last, after the parameter list
Number auto x Mixed no name to use: decltype(x) reaches the type Poor one concept before each auto, nothing more Poor invents a parameter of its own, so it cannot reach the class T Good double half(Number auto x) needs no template line
How the answers decide
  • “Must the body name the type, for example to declare a variable of type T?” Yes, the body uses T: adds a caveat to Number auto x. An auto parameter has no type name; decltype(x) reaches the type, but a named T reads better.
  • “Does the condition need more than one concept for a parameter, such as A<T> || B<T>?” Yes, it joins conditions: rules out template <Number T> and Number auto x. A concept in place of typename, or before auto, names one concept; && and || need a requires-clause.
  • “Is it a member function that constrains the T of its class template?” Yes, the class introduced T: rules out template <Number T>, A requires-clause and Number auto x. The class introduced T, and a plain member has no template line of its own: only the trailing form reaches the class T.

In short: the concept in place of typename for one concept on one parameter, a requires-clause for conditions joined with && or ||, and the trailing form for a member that constrains its class’s T.

Checkpoint

Check: The four forms of a constraint

  1. Question 1 of 2 double half(Number auto x) { return x / 2.0; } is which kind of declaration?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A function template whose invented parameter must satisfy Number

    A constrained auto parameter is the fourth form of a constraint: an abbreviated function template whose invented parameter carries the concept. It means the same as template <Number T> double half(T x).

  2. Question 2 of 2 concept Number = std::integral<T> || std::floating_point<T>; Which of these types satisfy it?

    Choose every answer that is right.

    A hint

    Two of the program's last lines answer it for you.

    Show the answer: Question 2 of 2

    Answer:

    • int
    • char
    • bool

    A concept checks exactly what it says. std::integral includes bool and the character types, so Number accepts them; to refuse bool, a concept has to say so, as NumberNotBool = Number<T> && !std::same_as<T, bool> does.

A constraint moves the error to your own call

A template without a constraint accepts any type until something in its body fails to compile for it. The failure is then reported where it happens, which for a function that calls the standard library is deep inside the library. Here is a median of temperature readings whose struct has no comparison operators, first unconstrained and then constrained with the standard concept std::totally_ordered:

median() without and with a constraint

median_plain.cpp

// median() without a constraint: the requirement "values can be compared with <" is hidden in std::sort.
#include <algorithm>
#include <vector>

struct Reading {
    double celsius;
};

template <typename T>
T median(std::vector<T> values) {
    std::sort(values.begin(), values.end());
    return values[values.size() / 2];
}

int main() {
    std::vector<Reading> day{{31.5}, {29.0}, {33.2}};
    Reading m = median(day);
    return static_cast<int>(m.celsius);
}

median_constrained.cpp

// The same median() with a standard concept: the requirement is part of the declaration.
#include <algorithm>
#include <concepts>
#include <vector>

struct Reading {
    double celsius;
};

template <std::totally_ordered T>
T median(std::vector<T> values) {
    std::sort(values.begin(), values.end());
    return values[values.size() / 2];
}

int main() {
    std::vector<Reading> day{{31.5}, {29.0}, {33.2}};
    Reading m = median(day);
    return static_cast<int>(m.celsius);
}

median_fixed.cpp

// The fix: give Reading the comparisons, and the constrained median() accepts it.
#include <algorithm>
#include <compare>
#include <concepts>
#include <print>
#include <vector>

struct Reading {
    double celsius;
    auto operator<=>(const Reading&) const = default;   // ==, !=, <, <=, > and >= from one line
};

template <std::totally_ordered T>
T median(std::vector<T> values) {
    std::sort(values.begin(), values.end());
    return values[values.size() / 2];
}

int main() {
    std::vector<Reading> day{{31.5}, {29.0}, {33.2}};
    std::println("median: {}", median(day).celsius);
}
Error lengths, the constrained errors, and the fix Python · median_session.py
from session import run

# How long is each error? awk counts the lines and the lines that say "error:".
run("""
clang++ -std=c++23 -c median_plain.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
clang++ -std=c++23 -c median_constrained.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
g++-16 -std=c++23 -c median_plain.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
g++-16 -std=c++23 -c median_constrained.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
# The whole constrained error from Clang, and the start of GCC's
clang++ -std=c++23 -c median_constrained.cpp
g++-16 -std=c++23 -c median_constrained.cpp
# With the comparisons added
clang++ -std=c++23 -Wall -Wextra median_fixed.cpp -o median_fixed && ./median_fixed
g++-16 -std=c++23 -Wall -Wextra median_fixed.cpp -o median_fixed-gcc && ./median_fixed-gcc
""")

Output

$ clang++ -std=c++23 -c median_plain.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
334 lines, 20 errors
$ clang++ -std=c++23 -c median_constrained.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
19 lines, 1 error
$ g++-16 -std=c++23 -c median_plain.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
1457 lines, 18 errors
$ g++-16 -std=c++23 -c median_constrained.cpp 2>&1 | awk '/error:/ { e++ } END { print NR " lines, " e (e == 1 ? " error" : " errors") }'
64 lines, 1 error
# The whole constrained error from Clang, and the start of GCC's
$ clang++ -std=c++23 -c median_constrained.cpp
median_constrained.cpp:18:17: error: no matching function for call to 'median'
   18 |     Reading m = median(day);
      |                 ^~~~~~
median_constrained.cpp:11:3: note: candidate template ignored: constraints not satisfied [with T = Reading]
   11 | T median(std::vector<T> values) {
      |   ^
median_constrained.cpp:10:11: note: because 'Reading' does not satisfy 'totally_ordered'
   10 | template <std::totally_ordered T>
      |           ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__concepts/totally_ordered.h:41:27: note: because 'Reading' does not satisfy 'equality_comparable'
   41 | concept totally_ordered = equality_comparable<_Tp> && __partially_ordered_with<_Tp, _Tp>;
      |                           ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__concepts/equality_comparable.h:39:31: note: because '__weakly_equality_comparable_with<Reading, Reading>' evaluated to false
   39 | concept equality_comparable = __weakly_equality_comparable_with<_Tp, _Tp>;
      |                               ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__concepts/equality_comparable.h:32:13: note: because '__t == __u' would be invalid: invalid operands to binary expression ('const __libcpp_remove_reference_t<Reading>' (aka 'const Reading') and 'const __libcpp_remove_reference_t<Reading>' (aka 'const Reading'))
   32 |       { __t == __u } -> __boolean_testable;
      |             ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c median_constrained.cpp
median_constrained.cpp: In function 'int main()':
median_constrained.cpp:18:23: error: no matching function for call to 'median(std::vector<Reading>&)'
   18 |     Reading m = median(day);
      |                 ~~~~~~^~~~~
  • there is 1 candidate
    • candidate 1: 'template<class T>  requires  totally_ordered<T> T median(std::vector<T>)'
      median_constrained.cpp:11:3:
         11 | T median(std::vector<T> values) {
            |   ^~~~~~
      • template argument deduction/substitution failed:
        • constraints not satisfied
In file included from /opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/compare:44,
                 from /opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/bits/stl_pair.h:65,
                 from /opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/bits/stl_algobase.h:63,
                 from /opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/algorithm:62,
                 from median_constrained.cpp:2:
          • /opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/concepts: In substitution of 'template<class T>  requires  totally_ordered<T> T median(std::vector<T>) [with T = Reading]':
          • required from here
            median_constrained.cpp:18:23:   
               18 |     Reading m = median(day);
                  |                 ~~~~~~^~~~~
          • required for the satisfaction of '__weakly_eq_cmp_with<_Tp, _Tp>' [with _Tp = Reading]
            /opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/concepts:325:15:   
              325 |       concept __weakly_eq_cmp_with
                  |               ^~~~~~~~~~~~~~~~~~~~
          • required for the satisfaction of 'equality_comparable<_Tp>' [with _Tp = Reading]

Text longer than 4,000 characters is cut here; run the example yourself to see all of it.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 median_session.py

The numbers are the point of the first four commands:

median(day) with Reading Apple clang 21 GCC 16
No constraint (median_plain.cpp) 334 lines, 20 errors 1,457 lines, 18 errors
std::totally_ordered (median_constrained.cpp) 19 lines, 1 error 64 lines, 1 error

Without a constraint, every error is inside the helpers that std::sort uses (__sift_down, __partial_sort and others in libc++; __move_median_to_first, __unguarded_partition and __insertion_sort in libstdc++). Your own two lines, the call to std::sort and the call to median, appear only in the notes that lead to each error. Clang’s 20 is its limit rather than the whole count: by default it stops after 20 errors (-ferror-limit). With the constraint, both compilers stop at the call, median(day), with exactly one error. Read it from the top, one line at a time, down the concept’s definition to the operation that is missing:

Interactive · stage by stage Reading the one error a constraint gives, from the call down Step through Clang's message for median(day). Each stage is one line of it, from your call down the concept's definition to the operation that is missing.
Reading the one error a constraint gives, from the call down, step 7 of 7. Showing The call, line 18, The candidate is ignored, totally_ordered fails, equality_comparable fails, __t == __u is invalid, The fix. In focus: The fix.The call, line 18Reading m = median(day);The candidate is ignoredconstraints not satisfiedtotally_ordered failsthe constraint on line 10equality_comparable failsthe first concept it builds on__t == __u is invalidno == for two ReadingsThe fixa defaulted operator<=>

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The error is at your call. Clang's one error, no matching function for call to 'median', points at line 18 of median_constrained.cpp: the call in main. Without the constraint, the errors pointed into the library.
  2. The candidate is ignored. The only candidate, the median template, was ignored because its constraints are not satisfied with T = Reading. Its body was never compiled for Reading, so std::sort never reached the missing <.
  3. The constraint you wrote. The next note names the concept and the line that states it: 'Reading' does not satisfy 'totally_ordered', at line 10, your template line.
  4. Down into its definition. totally_ordered is defined as equality_comparable together with the four orderings, and Clang follows the definition down, one note a level: 'Reading' does not satisfy 'equality_comparable'.
  5. The expression that fails. The last note shows the requirement itself: '__t == __u' would be invalid, with invalid operands for two const Readings. The operation reported missing is ==, not the < that std::sort uses.
  6. GCC tells the same chain. GCC 16 prints the chain as an indented tree, required for the satisfaction of equality_comparable, then of totally_ordered, lists each invalid comparison and ends with a hint: set '-fconcepts-diagnostics-depth=' to at least 2.
  7. Give Reading the comparisons. A defaulted operator<=> gives Reading all six comparisons, == among them. The constraint is satisfied, std::sort gets its <, and median_fixed.cpp printed median: 31.5 on both compilers.

Clang follows the definition one note per level: totally_ordered needs equality_comparable, which needs __t == __u to compile, and that expression “would be invalid”. GCC 16 prints the same chain as an indented tree, a new layout in this release, and ends with a hint: -fconcepts-diagnostics-depth=2 makes it explain one level deeper.

The message also teaches something about the concept. std::sort uses only <, yet std::totally_ordered asks for == and != as well as all four of <, >, <= and >=, so the first thing reported missing is ==. A standard concept describes a meaningful set of operations, not the exact expressions one body happens to use. The fix in median_fixed.cpp is one line: a defaulted operator<=>, the three-way comparison: The operator <=> (C++20): one comparison that says whether a value is less than, equal to or greater than another, or unordered with it. Defaulted, it compares the members in declaration order, and the compiler rewrites <, <=, > and >= in terms of it. More in “C++ structs, aggregates and designated initialisers”, gives Reading all six comparisons, the constraint is satisfied and the median, 31.5, comes out on both compilers. The lesson on reading compiler errors compared std::sort with the constrained std::ranges::sort on a std::list: there too both compilers named the requirement that failed, though only Clang’s message got shorter.

Text Diff Paste the unconstrained and the constrained error side by side to see which lines name your own code.

Checkpoint

Check: The error at the call

  1. Question 1 of 2 With template <std::totally_ordered T>, where do both compilers report the error for median(day)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: At the call in main, line 18, with one error

    A constraint is part of the template's declaration, so the compiler checks it at the call and rejects the candidate there, without instantiating the body. Your own line becomes the place of the error.

  2. Question 2 of 2 Which one change to Reading makes median(day) compile against std::totally_ordered?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: auto operator<=>(const Reading&) const = default;

    std::totally_ordered asks for all six comparisons. One defaulted operator<=> supplies them, comparing the members in order, so the constraint is satisfied and std::sort gets its <.

A requires-expression lists what must compile

When no standard concept says what a template needs, a requires-expression: An expression of type bool, requires (parameters) { requirements }, that is true when every requirement holds: expressions that must compile, types that must exist, result types to check and nested conditions that must be true. Its body is checked, never run. spells it out. Its optional parameter list, requires (T& buf, const T& cbuf, std::size_t i), introduces names for hypothetical objects that are never created; the braces then hold requirements, and the expression is true when all of them are met. There are four kinds, all used in one concept here:

requirements.cpp C++ · requirements.cpp
// A concept built from the four kinds of requirement a requires-expression can hold.
#include <array>
#include <concepts>
#include <cstddef>
#include <list>
#include <print>
#include <string>
#include <type_traits>
#include <vector>

// A buffer that can be indexed, measured and cleared, whose elements can be copied byte by byte.
template <typename T>
concept TrivialBuffer = requires(T& buf, const T& cbuf, std::size_t i) {
    typename T::value_type;                                         // type requirement
    buf.clear();                                                    // simple requirement
    { cbuf.size() } -> std::convertible_to<std::size_t>;            // compound requirement
    { cbuf[i] } -> std::same_as<const typename T::value_type&>;     // compound requirement
    requires std::is_trivially_copyable_v<typename T::value_type>;  // nested requirement
};

// A common slip: without "requires" in front, the trait is a simple requirement, which only
// checks that the expression compiles, never whether it is true.
template <typename T>
concept AlmostTrivialBuffer = requires {
    typename T::value_type;
    std::is_trivially_copyable_v<typename T::value_type>;            // compiles for any value_type
};

int main() {
    std::println("std::vector<int>          {}", TrivialBuffer<std::vector<int>>);
    std::println("std::string               {}", TrivialBuffer<std::string>);
    std::println("std::vector<std::string>  {}", TrivialBuffer<std::vector<std::string>>);
    std::println("std::array<int, 4>        {}", TrivialBuffer<std::array<int, 4>>);
    std::println("std::list<int>            {}", TrivialBuffer<std::list<int>>);
    std::println("std::vector<bool>         {}", TrivialBuffer<std::vector<bool>>);
    std::println("int                       {}", TrivialBuffer<int>);
    std::println("AlmostTrivialBuffer<std::vector<std::string>>: {}", AlmostTrivialBuffer<std::vector<std::string>>);
}
Which containers are a TrivialBuffer? Python · requirements_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra requirements.cpp -o requirements && ./requirements
g++-16 -std=c++23 -Wall -Wextra requirements.cpp -o requirements-gcc && ./requirements-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra requirements.cpp -o requirements && ./requirements
std::vector<int>          true
std::string               true
std::vector<std::string>  false
std::array<int, 4>        false
std::list<int>            false
std::vector<bool>         false
int                       false
AlmostTrivialBuffer<std::vector<std::string>>: true
$ g++-16 -std=c++23 -Wall -Wextra requirements.cpp -o requirements-gcc && ./requirements-gcc
std::vector<int>          true
std::string               true
std::vector<std::string>  false
std::array<int, 4>        false
std::list<int>            false
std::vector<bool>         false
int                       false
AlmostTrivialBuffer<std::vector<std::string>>: true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 requirements_session.py

  • A simple requirement, buf.clear();, is an expression that must compile. It is never run.
  • A type requirement, typename T::value_type;, needs the named type to exist.
  • A compound requirement, { cbuf.size() } -> std::convertible_to<std::size_t>;, needs the expression to compile and its type to satisfy the concept after the arrow. The compiler passes the expression’s type as that concept’s first argument, so std::same_as<const typename T::value_type&> checks the exact type of cbuf[i]. Writing noexcept after the braces also requires that the expression cannot throw.
  • A nested requirement, requires std::is_trivially_copyable_v<…>;, evaluates a constant boolean and needs it to be true.

The compiler checks the requirements in the order they are written and stops at the first that fails, which decides the answer. Step through the seven types the program asks about and watch where each one stops:

Interactive · stage by stage Where each type stops in the five requirements of TrivialBuffer Step through one type at a time. The requirements are checked in the order written, and the first one that fails decides the answer; the last stage is what the program printed.
Where each type stops in the five requirements of TrivialBuffer, step 8 of 8. Showing typename T::value_type (std::string), buf.clear(); (not in this concept), { cbuf.size() } (not in this concept), { cbuf[i] } (not in this concept), requires trivially copyable (only compiled, never tested), TrivialBuffer<T> (true, wrongly). In focus: requires trivially copyable, TrivialBuffer<T>.typename T::value_typestd::stringbuf.clear();not in this concept{ cbuf.size() }not in this concept{ cbuf[i] }not in this conceptrequires trivially copyableonly compiled, never testedTrivialBuffer<T>true, wrongly

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. std::vector<int>. A vector of ints has a value_type, clear() and size(), its const operator[] gives a const int&, and an int can be copied byte by byte. Every requirement holds: true.
  2. std::string. A string is a buffer of chars. Its const operator[] gives a const char&, and a char is trivially copyable, so the string passes all five: true.
  3. std::vector<std::string>. The first four requirements hold. The nested one fails: a std::string has a copy constructor of its own and cannot be copied byte by byte, so the answer is false.
  4. std::array<int, 4>. An array has a value_type, but its size is fixed and it has no clear(). The second requirement fails, checking stops there, and the answer is false.
  5. std::list<int>. A list can be cleared and measured, but it has no operator[]: its elements are reached one step at a time. The fourth requirement fails: false.
  6. std::vector<bool>. This vector packs its elements as bits, so its const operator[] returns a value, a bool with libstdc++ and a proxy object with libc++, never a const bool&. The same_as check of the fourth requirement fails: false.
  7. int. An int is no container at all. The very first requirement fails, because int has no member type value_type, and nothing after it is checked: false.
  8. The trait without requires. AlmostTrivialBuffer keeps only the type requirement and writes the trait without requires in front. That makes it a simple requirement: it only has to compile, and it does, so std::vector<std::string> gets true.

The results explain themselves once you know where each type falls short. std::vector<std::string> has every operation, but a std::string cannot be copied byte by byte, so the nested requirement fails. std::array has no clear(), std::list has no operator[], and int has no value_type. std::vector<bool> is the subtle one: it packs its elements as bits, so there is no bool object for operator[] to refer to. On a const vector it returns a value instead, a bool with libstdc++ and a small proxy object with libc++, and the std::same_as check rejects both.

A trait without requires in front

In AlmostTrivialBuffer the trait is written without requires, which makes it a simple requirement: the compiler checks only that std::is_trivially_copyable_v<std::string> is a valid expression, which it always is, and never looks at its value. So std::vector<std::string> passes without a warning. Write requires before every condition that must be true.

A requires-expression may also appear directly in a requires-clause, which gives the odd-looking template <typename T> requires requires (T t) { t.size(); }. The first requires starts the clause and the second starts the expression. It is legal, but a constraint worth writing once is usually worth naming as a concept.

Checkpoint

Check: Writing a requires-expression

  1. Question 1 of 2 Which kind of requirement is { cbuf.size() } -> std::convertible_to<std::size_t>;?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A compound requirement

    The four kinds read differently: an expression for a simple requirement, typename for a type requirement, braces with an optional arrow and concept for a compound requirement, and requires for a nested one.

  2. Question 2 of 2 Why did the program print false for TrivialBuffer<std::array<int, 4>>?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: std::array has no clear(), so the simple requirement buf.clear(); fails

    The requirements are checked in the order written, and the first one that fails decides. For std::array that is buf.clear();, an expression that does not compile.

When two overloads match, the more constrained one wins

Constraints take part in overload resolution: How the compiler chooses the function a call means: it keeps the functions of that name the arguments can be converted for, ranks each conversion (exact match, promotion, conversion) and picks the one best, or reports an ambiguous call. More in “C++ function overloading and default arguments”. The compiler first drops every candidate whose constraints are not satisfied. If two candidates remain and the usual rules cannot choose between them, the one whose constraints subsumption: How the compiler orders two constrained overloads that are otherwise equally good: a constraint subsumes another when it contains each of the other's atomic constraints, as std::signed_integral<T> contains std::integral<T> and adds one, and the overload whose constraint subsumes is chosen. the other’s wins: it is more constrained, because its condition includes the other’s condition and adds to it.

subsume.cpp C++ · subsume.cpp
// Three overloads. For a signed integer two of them are viable, and the more constrained one wins.
#include <concepts>
#include <print>

void describe(std::integral auto) { std::println("an integer"); }
void describe(std::signed_integral auto) { std::println("a signed integer"); }
void describe(std::floating_point auto) { std::println("a floating-point number"); }

int main() {
    describe(42);     // int
    describe(42u);    // unsigned int
    describe('A');    // char: signed on some platforms, unsigned on others
    describe(true);   // bool
    describe(2.5);    // double
}
Five calls, three overloads Python · subsume_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra subsume.cpp -o subsume && ./subsume
g++-16 -std=c++23 -Wall -Wextra subsume.cpp -o subsume-gcc && ./subsume-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra subsume.cpp -o subsume && ./subsume
a signed integer
an integer
a signed integer
an integer
a floating-point number
$ g++-16 -std=c++23 -Wall -Wextra subsume.cpp -o subsume-gcc && ./subsume-gcc
a signed integer
an integer
a signed integer
an integer
a floating-point number

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 subsume_session.py

std::signed_integral<T> is defined in the standard as std::integral<T> && std::is_signed_v<T>, so for int both of the first two overloads are viable and the signed one, which subsumes the other, is called. Step through the five calls of main and watch which overloads each argument reaches:

Interactive · step by step Five calls of describe and the overload each one gets Step through main of subsume.cpp one call at a time. The arrows go to the overloads whose constraints the argument satisfies; the sub-label says which one was called.
Five calls of describe and the overload each one gets, step 7 of 7. Showing describe(42) (printed: a signed integer), describe(42u) (printed: an integer), describe('A') (printed: a signed integer), describe(true) (printed: an integer), describe(2.5) (printed: floating point), signed_integral auto, integral auto, floating_point auto.describe(42)printed: a signed integerdescribe(42u)printed: an integerdescribe('A')printed: a signed integerdescribe(true)printed: an integerdescribe(2.5)printed: floating pointsigned_integral autoa signed integerintegral autoan integerfloating_point autoa floating-point number

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Three overloads. subsume.cpp declares three describe overloads, each an abbreviated template with a constrained auto. signed_integral is defined as integral together with is_signed_v, so it says more than integral.
  2. Two overloads fit 42. An int is integral and signed, so two overloads are viable, and their parameters are the same. Only the constraints can decide: signed_integral includes integral and adds a condition, so it is more constrained and wins.
  3. One overload fits 42u. An unsigned int satisfies integral but not signed_integral, so only one overload is viable, and it is called: the program printed an integer.
  4. A char, on this Mac. Plain char is signed on Apple's platforms, so 'A' fits both integer overloads, like 42, and the signed one wins. Compiled for Arm Linux, where char is unsigned, the same call prints an integer.
  5. A bool is not signed. bool is an integral type, but is_signed_v<bool> is false, so signed_integral rejects it and only the integral overload is viable: an integer.
  6. A double. A double satisfies only floating_point, so the third overload is the one candidate left. The program printed a floating-point number on both compilers.
  7. Five calls, three overloads. Where one overload was viable, it was called. Where two were, the one whose constraint includes the other won. With no overload viable, the call would be an error naming the failed constraints.

42u satisfies only std::integral, and so does true: bool is integral but not signed. The char call says “a signed integer” on this Mac because Apple’s platforms make plain char signed; on Arm Linux, where char is unsigned, the same program prints “an integer”. The next lesson shows how to ask that question of the target directly. The whole decision, traced for describe(42), fits in one picture:

How the compiler picks an overload: drop candidates whose constraints fail, then prefer the one whose constraint subsumes the other.describe(42):T = int forevery candidateCheck each constraint:integral<int>: yessigned_integral<int>: yesfloating_point<int>: no,so it is droppedAny candidate left?No: error, namingthe failed constraintExactly one left?Yes: call itTwo left: does oneconstraint subsumethe other?Yes: signed_integralsubsumes integral,so it wins:"a signed integer"No: the callis ambiguousnoyesyesnoyesno

Choosing between constrained overloads, traced for describe(42)

Text description of the diagram

The diagram follows the call describe(42) from subsume.cpp, which has three overloads constrained by std::integral, std::signed_integral and std::floating_point.

  1. Deduction gives T = int for every candidate.
  2. The compiler checks each candidate's constraints. integral<int> and signed_integral<int> are satisfied; floating_point<int> is not, so that candidate is dropped.
  3. If no candidate is left, the call is an error that names the constraint that was not satisfied. If exactly one is left, it is called.
  4. If two or more are left and the other overload rules cannot choose, the compiler asks whether one candidate's constraint subsumes the other's. signed_integral is defined as integral<T> && is_signed_v<T>, so it subsumes integral and wins: the program prints "a signed integer".
  5. If neither subsumes the other, the call is ambiguous. That also happens when the same trait is written out in two places instead of being shared through one concept.

Key idea

Constraints first remove the overloads a type cannot use; when two remain and are otherwise equal, the one whose constraint includes the other’s, as signed_integral includes integral, is called.

Checkpoint

Check: The more constrained overload wins

  1. Question 1 of 2 What makes std::signed_integral more constrained than std::integral?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its definition is std::integral<T> && std::is_signed_v<T>, so it includes integral's condition and adds one

    When two overloads are otherwise equally good, the compiler picks the one whose constraint subsumes the other's. std::signed_integral is built from std::integral, so it subsumes it, and describe(42) printed a signed integer.

  2. Question 2 of 2 On this Mac describe('A') printed a signed integer. What does the same program print when it is built for 64-bit Arm Linux?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: an integer

    The overload set is the same, but the facts about char differ by target. A constraint answers for the platform the program is compiled for, which is why 'A' behaves like 42 on the Mac and like 42u on Arm Linux.

Subsumption sees only constraints written once

Subsumption compares atomic constraint: One of the smallest pieces a constraint splits into at && and ||, such as std::is_signed_v<T>. Two atomic constraints count as the same only when they come from the same expression written in one place, normally inside one concept., the smallest pieces a constraint splits into at && and ||. The standard counts two atomic constraints as the same only when they come from the same appearance of the same expression in the source. Writing a trait twice, even character for character, gives two different constraints:

A repeated trait, and the same rule built from concepts

ambiguous.cpp

// The same trait written in two places: two different atomic constraints, so neither overload is more constrained.
#include <print>
#include <type_traits>

template <typename T>
    requires std::is_integral_v<T>
void show(T) { std::println("integral"); }

template <typename T>
    requires std::is_integral_v<T> && std::is_signed_v<T>
void show(T) { std::println("signed integral"); }

int main() {
    show(42);
}

ambiguous_fixed.cpp

// The fix: name the shared part as a concept and build the stricter concept from it.
#include <print>
#include <type_traits>

template <typename T>
concept Integer = std::is_integral_v<T>;

template <typename T>
concept SignedInteger = Integer<T> && std::is_signed_v<T>;

void show(Integer auto) { std::println("integral"); }
void show(SignedInteger auto) { std::println("signed integral"); }

int main() {
    show(42);
    show(42u);
}
An ambiguous call, then the fix Python · ambiguous_session.py
from session import run

run("""
clang++ -std=c++23 -c ambiguous.cpp
g++-16 -std=c++23 -c ambiguous.cpp
clang++ -std=c++23 -Wall -Wextra ambiguous_fixed.cpp -o ambiguous_fixed && ./ambiguous_fixed
g++-16 -std=c++23 -Wall -Wextra ambiguous_fixed.cpp -o ambiguous_fixed-gcc && ./ambiguous_fixed-gcc
""")

Output

$ clang++ -std=c++23 -c ambiguous.cpp
ambiguous.cpp:14:5: error: call to 'show' is ambiguous
   14 |     show(42);
      |     ^~~~
ambiguous.cpp:7:6: note: candidate function [with T = int]
    7 | void show(T) { std::println("integral"); }
      |      ^
ambiguous.cpp:11:6: note: candidate function [with T = int]
   11 | void show(T) { std::println("signed integral"); }
      |      ^
ambiguous.cpp:6:14: note: similar constraint expressions not considered equivalent; constraint expressions cannot be considered equivalent unless they originate from the same concept
    6 |     requires std::is_integral_v<T>
      |              ^~~~~~~~~~~~~~~~~~~~~
ambiguous.cpp:10:14: note: similar constraint expression here
   10 |     requires std::is_integral_v<T> && std::is_signed_v<T>
      |              ^~~~~~~~~~~~~~~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c ambiguous.cpp
ambiguous.cpp: In function 'int main()':
ambiguous.cpp:14:9: error: call of overloaded 'show(int)' is ambiguous
   14 |     show(42);
      |     ~~~~^~~~
  • there are 2 candidates
    • candidate 1: 'void show(T) [with T = int]'
      ambiguous.cpp:7:6:
          7 | void show(T) { std::println("integral"); }
            |      ^~~~
    • candidate 2: 'void show(T) [with T = int]'
      ambiguous.cpp:11:6:
         11 | void show(T) { std::println("signed integral"); }
            |      ^~~~
[exit status 1]
$ clang++ -std=c++23 -Wall -Wextra ambiguous_fixed.cpp -o ambiguous_fixed && ./ambiguous_fixed
signed integral
integral
$ g++-16 -std=c++23 -Wall -Wextra ambiguous_fixed.cpp -o ambiguous_fixed-gcc && ./ambiguous_fixed-gcc
signed integral
integral

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ambiguous_session.py

To the reader, the second show obviously refines the first. To the compiler, the two std::is_integral_v<T> are unrelated, so neither overload is more constrained and show(42) is ambiguous on both compilers. Clang even explains it: “constraint expressions cannot be considered equivalent unless they originate from the same concept”. Step through the pieces of both versions:

Interactive · step by step Why the repeated trait is ambiguous and the concepts are not Step through the constraints of the two show overloads, split into their smallest pieces. The left column is ambiguous.cpp, the right one ambiguous_fixed.cpp.
Why the repeated trait is ambiguous and the concepts are not, step 6 of 6. Showing show #1, line 6 (one atom, written on line 6), show #2, line 10 (two atoms, written on line 10), show(42) (ambiguous on both compilers), concept Integer (one atom, written once), concept SignedInteger (Integer’s atom and one more), show(42), fixed (printed: signed integral). In focus: show(42), fixed.same textcontains itshow #1, line 6one atom, written on line 6show #2, line 10two atoms, written on line 10show(42)ambiguous on both compilersconcept Integerone atom, written onceconcept SignedIntegerInteger’s atom and one moreshow(42), fixedprinted: signed integral

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first constraint. The first show requires std::is_integral_v<T>, written on line 6. A constraint splits at && and || into atomic constraints, and this one is a single atom.
  2. The second constraint. The second show requires std::is_integral_v<T> && std::is_signed_v<T>, written on line 10. It splits into two atoms, and the first has the same text as line 6.
  3. Same text, two atoms. Atoms are the same only when they come from one written expression. Line 6 and line 10 are two, so neither constraint includes the other, and both compilers call show(42) ambiguous.
  4. Name the shared part. The fix writes std::is_integral_v<T> once, as the body of the concept Integer. Every use of Integer<T> now means that one atom.
  5. Build on the concept. SignedInteger is Integer<T> && std::is_signed_v<T>. Its atoms are the atom inside Integer and one more, so it includes every atom of Integer: SignedInteger subsumes Integer.
  6. The call is decided. show(42) now has a more constrained candidate, and the program printed signed integral, then integral for show(42u), which satisfies Integer only.

The fix names the shared part once, as the concept Integer, and builds SignedInteger from it; now SignedInteger contains the very same Integer<T>, subsumption sees it, and show(42) picks the signed overload while show(42u) picks the other. Build families of concepts out of each other, the way the standard builds std::signed_integral from std::integral.

C++26: constraints that fold over a pack

The previous lesson used fold expression: A C++17 expression in parentheses, such as (args + ...) or (... - args), that joins every element of a pack with one binary operator. The side of the ... decides the grouping, and a binary fold such as (args + ... + 0) adds a starting value. More in “Variadic templates and fold expressions” over packs. A fold can be a constraint too, requires (std::integral<T> && ...), but in C++20 and C++23 the whole fold is one atomic constraint, so a fold of std::signed_integral does not subsume a fold of std::integral. C++26 changes that (paper P2963R3):

fold_constraints.cpp C++ · fold_constraints.cpp
// Two constraints written as fold expressions over a pack. Which overload does f(1, 2) call?
#include <concepts>
#include <print>

template <typename... T>
    requires (std::integral<T> && ...)
void f(T...) { std::println("all integral"); }

template <typename... T>
    requires (std::signed_integral<T> && ...)
void f(T...) { std::println("all signed"); }

int main() {
    f(1, 2);
}
The same call in C++23 and C++26 Python · fold_constraints_session.py
from session import run

run("""
clang++ -std=c++23 -c fold_constraints.cpp 2>&1 | grep error
clang++ -std=c++2c -Wall -Wextra fold_constraints.cpp -o fold_constraints && ./fold_constraints
g++-16 -std=c++26 -c fold_constraints.cpp 2>&1 | grep error
""")

Output

$ clang++ -std=c++23 -c fold_constraints.cpp 2>&1 | grep error
fold_constraints.cpp:14:5: error: call to 'f' is ambiguous
1 error generated.
$ clang++ -std=c++2c -Wall -Wextra fold_constraints.cpp -o fold_constraints && ./fold_constraints
all signed
$ g++-16 -std=c++26 -c fold_constraints.cpp 2>&1 | grep error
fold_constraints.cpp:14:6: error: call of overloaded 'f(int, int)' is ambiguous

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fold_constraints_session.py

With -std=c++23 the call f(1, 2) is ambiguous. With -std=c++2c, Clang applies the new rule and calls the “all signed” overload; Clang’s status page lists the paper from Clang 19. GCC 16 does not implement it yet, so its C++26 mode still reports the ambiguity. Code that must build on both compilers today can use the rule of this section instead: write each fold once, inside a concept, and build the stricter concept from the other.

fold_named.cpp C++ · fold_named.cpp
// The portable form today: name each fold once, as a concept, and build the stricter concept from it.
#include <concepts>
#include <print>
#include <type_traits>

template <typename... T>
concept AllIntegral = (std::integral<T> && ...);

template <typename... T>
concept AllSigned = AllIntegral<T...> && (std::is_signed_v<T> && ...);

template <typename... T>
    requires AllIntegral<T...>
void f(T...) { std::println("all integral"); }

template <typename... T>
    requires AllSigned<T...>
void f(T...) { std::println("all signed"); }

int main() {
    f(1, 2);     // two ints: both overloads fit, and AllSigned contains AllIntegral
    f(1u, 2);    // an unsigned int: only AllIntegral fits
}
Folds named as concepts, in C++23 on both compilers Python · fold_named_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra fold_named.cpp -o fold_named && ./fold_named
g++-16 -std=c++23 -Wall -Wextra fold_named.cpp -o fold_named-gcc && ./fold_named-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra fold_named.cpp -o fold_named && ./fold_named
all signed
all integral
$ g++-16 -std=c++23 -Wall -Wextra fold_named.cpp -o fold_named-gcc && ./fold_named-gcc
all signed
all integral

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fold_named_session.py

AllSigned contains AllIntegral<T...>, the one atom inside AllIntegral, so it subsumes it in C++23 already: f(1, 2) prints “all signed” and f(1u, 2) “all integral” on both compilers.

Checkpoint

Check: Atomic constraints, written once

  1. Question 1 of 2 When do two atomic constraints count as the same, for subsumption?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When they come from the same expression written in one place, such as the body of one concept

    The standard counts two atomic constraints as identical only when they are formed from the same appearance of the same expression. Naming a condition once, as a concept, is how two constraints come to share it.

  2. Question 2 of 2 Which change makes show(42) in ambiguous.cpp call the signed overload?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Write the trait once as concept Integer, and build the signed constraint as Integer<T> && std::is_signed_v<T>

    The cure is to give the shared condition one home. The standard does the same: std::signed_integral is built from std::integral, which is why it subsumes it.

Standard concepts check that code compiles, not its meaning

The header <concepts> defines the core concepts and <ranges> adds those for ranges, in namespace std::ranges. This program asks seven of them about nine types:

concept_table.cpp C++ · concept_table.cpp
// Seven standard concepts asked of nine types; the compiler answers every question.
#include <compare>
#include <concepts>
#include <list>
#include <print>
#include <ranges>
#include <string>
#include <vector>

struct Reading { double celsius; };                       // no comparisons
struct Celsius {
    double value;
    auto operator<=>(const Celsius&) const = default;      // all six comparisons
};
using Doubler = decltype([](int n) { return 2 * n; });     // the type of a lambda

template <typename T>
void row(const char* name) {
    auto yes = [](bool b) { return b ? "yes" : "-"; };
    std::println("{:<17}{:<10}{:<10}{:<9}{:<10}{:<11}{:<7}{}", name, yes(std::integral<T>),
                 yes(std::floating_point<T>), yes(std::totally_ordered<T>), yes(std::copyable<T>),
                 yes(std::invocable<T, int>), yes(std::ranges::range<T>),
                 yes(std::ranges::random_access_range<T>));
}

int main() {
    std::println("{:<17}{:<10}{:<10}{:<9}{:<10}{:<11}{:<7}{}", "", "integral", "floating", "ordered",
                 "copyable", "invocable", "range", "random");
    row<int>("int");
    row<bool>("bool");
    row<double>("double");
    row<std::string>("std::string");
    row<std::vector<int>>("std::vector<int>");
    row<std::list<int>>("std::list<int>");
    row<Reading>("Reading");
    row<Celsius>("Celsius");
    row<Doubler>("a lambda(int)");
}
Seven concepts, nine types Python · concept_table_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra concept_table.cpp -o concept_table && ./concept_table
g++-16 -std=c++23 -Wall -Wextra concept_table.cpp -o concept_table-gcc && ./concept_table-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra concept_table.cpp -o concept_table && ./concept_table
                 integral  floating  ordered  copyable  invocable  range  random
int              yes       -         yes      yes       -          -      -
bool             yes       -         yes      yes       -          -      -
double           -         yes       yes      yes       -          -      -
std::string      -         -         yes      yes       -          yes    yes
std::vector<int> -         -         yes      yes       -          yes    yes
std::list<int>   -         -         yes      yes       -          yes    -
Reading          -         -         -        yes       -          -      -
Celsius          -         -         yes      yes       -          -      -
a lambda(int)    -         -         yes      yes       yes        -      -
$ g++-16 -std=c++23 -Wall -Wextra concept_table.cpp -o concept_table-gcc && ./concept_table-gcc
                 integral  floating  ordered  copyable  invocable  range  random
int              yes       -         yes      yes       -          -      -
bool             yes       -         yes      yes       -          -      -
double           -         yes       yes      yes       -          -      -
std::string      -         -         yes      yes       -          yes    yes
std::vector<int> -         -         yes      yes       -          yes    yes
std::list<int>   -         -         yes      yes       -          yes    -
Reading          -         -         -        yes       -          -      -
Celsius          -         -         yes      yes       -          -      -
a lambda(int)    -         -         yes      yes       yes        -      -

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 concept_table_session.py

Concept Satisfied when
std::same_as<T, U> T and U are the same type
std::convertible_to<From, To> a From converts to a To, implicitly and explicitly
std::derived_from<D, B> D is the class B or is publicly derived from it
std::integral<T>, std::signed_integral<T>, std::unsigned_integral<T> T is an integer type, including bool and the character types
std::floating_point<T> T is float, double or long double (or an extended floating-point type)
std::equality_comparable<T>, std::totally_ordered<T> == and !=, or all six comparisons, work on T
std::copyable<T>, std::regular<T> T can be copied; regular adds default construction and ==
std::invocable<F, Args...>, std::predicate<F, Args...> F can be called with Args; a predicate’s result works as a bool
std::ranges::range<R>, std::ranges::random_access_range<R> R has begin and end; random access adds jumping to any element

Three rows of the program’s table are worth a second look. bool counts as integral, as average showed. std::list is a range but not a random-access range, which is why the constrained std::ranges::sort refuses it. And the lambda type counts as “ordered”: a lambda without captures converts to a function pointer, and pointers can be compared, so every expression std::totally_ordered asks for compiles.

That last row shows what a compiler can and cannot check. A type satisfies a concept when the required expressions compile. It models the concept only when it also meets the concept’s semantic requirement: A condition a concept states in words that no compiler checks, such as < being a strict ordering for std::totally_ordered. A type satisfies a concept when the required expressions compile; it models the concept only if it meets these conditions too., which the standard states in words: < must be a strict ordering, == an equivalence. Compilers check only satisfaction, and a program that relies on a concept that is satisfied but not modelled is ill-formed, with no diagnostic required. The standard gives its own example: the < of double does not meet the semantics of std::totally_ordered for NaN: Not a number: the floating-point result of an operation with no sensible answer, such as infinity minus infinity. Every ordered comparison with a NaN is false, NaN == NaN is false too, and std::isnan tests for it. More in “Floating-point precision and comparison in C++” values, yet double still models the concept, because required operations need not work for every possible value. A concept cannot stop a NaN from reaching your sort.

Checkpoint

Check: Standard concepts, satisfied and modelled

  1. Question 1 of 2 According to the table the program printed, why does the constrained std::ranges::sort refuse a std::list<int>?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A list is a range but not a random-access range

    std::ranges::random_access_range adds jumping to any element to std::ranges::range. std::string and std::vector have it, std::list does not, and the constrained sort says which requirement failed.

  2. Question 2 of 2 std::totally_ordered<double> is true, yet both x < nan and nan < x are false. What does the standard say about double?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: double still models the concept, because required operations need not work for every value

    Satisfying a concept means that its expressions compile; modelling it means also meeting its meaning. Compilers check only the first. For double the standard says the concept is modelled even though NaN breaks <, so a program that may meet a NaN has to deal with it itself.

Practise: a Shape concept

shapes.h holds a starter for two pieces of a small geometry library:

  1. Shape: a type is a shape when, on a const object, area() gives something that converts to double and name() gives something that converts to std::string_view.
  2. total_area(shapes): the sum of the areas of a std::vector of shapes, or 0 for an empty vector.

The sample tests check the concept with static_assert, which the compiler evaluates before the program exists, and then call total_area:

The starter header and its sample tests

C header · shapes.h

// Starter code for "Your turn": write the Shape concept and total_area().
#pragma once
#include <concepts>
#include <string_view>
#include <vector>

// TODO 1: a Shape has area(), whose result converts to double, and name(), whose result
// converts to std::string_view; both must work on a const object.
template <typename T>
concept Shape = true;

// TODO 2: the sum of the areas of all the shapes, 0 when there are none.
template <Shape S>
double total_area(const std::vector<S>& shapes) {
    (void)shapes;
    return 0.0;
}

C++ · shapes_tests.cpp

// Sample tests for shapes.h. Build: clang++ -std=c++23 -Wall -Wextra shapes_tests.cpp -o shapes_tests
#include <cmath>
#include <numbers>
#include <print>
#include <string>
#include <string_view>
#include <vector>
#include "shapes.h"

struct Square {
    double side;
    double area() const { return side * side; }
    std::string name() const { return "square"; }
};
struct Circle {
    double radius;
    double area() const { return std::numbers::pi * radius * radius; }
    std::string_view name() const { return "circle"; }
};
struct Label {   // a name, but no area
    std::string text;
    std::string name() const { return text; }
};
struct Blob {    // an area, but no name
    double area() const { return 1.0; }
};
struct Tile {    // area() gives text, not a number
    std::string area() const { return "large"; }
    std::string name() const { return "tile"; }
};

// The compiler checks these lines before the program exists.
static_assert(Shape<Square>, "a Square is a Shape");
static_assert(Shape<Circle>, "a Circle is a Shape");
static_assert(!Shape<Label>, "a type without area() is rejected");
static_assert(!Shape<Blob>, "a type without name() is rejected");
static_assert(!Shape<Tile>, "area() must give a number");
static_assert(!Shape<int>, "an int is not a Shape");

int checks = 0;
int failures = 0;

void check(bool ok, const char* what) {
    ++checks;
    if (!ok) {
        ++failures;
        std::println("FAIL {}", what);
    }
}

int main() {
    check(total_area(std::vector<Square>{{2.0}, {3.0}}) == 13.0, "two squares: 4 + 9");
    check(std::abs(total_area(std::vector<Circle>{{1.0}, {2.0}}) - 5 * std::numbers::pi) < 1e-9, "two circles: 5 pi");
    check(total_area(std::vector<Square>{}) == 0.0, "no shapes at all");
    std::println("{} of {} checks passed", checks - failures, checks);
    return failures == 0 ? 0 : 1;
}
The sample tests against the starter code Python · shapes_session.py
from session import run

# The sample tests, built against the starter code before any TODO is done.
run("""
clang++ -std=c++23 -Wall -Wextra shapes_tests.cpp -o shapes_tests && ./shapes_tests
g++-16 -std=c++23 -Wall -Wextra shapes_tests.cpp -o shapes_tests-gcc && ./shapes_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra shapes_tests.cpp -o shapes_tests && ./shapes_tests
shapes_tests.cpp:35:15: error: static assertion failed due to requirement '!Shape<Label>': a type without area() is rejected
   35 | static_assert(!Shape<Label>, "a type without area() is rejected");
      |               ^~~~~~~~~~~~~
shapes_tests.cpp:36:15: error: static assertion failed due to requirement '!Shape<Blob>': a type without name() is rejected
   36 | static_assert(!Shape<Blob>, "a type without name() is rejected");
      |               ^~~~~~~~~~~~
shapes_tests.cpp:37:15: error: static assertion failed due to requirement '!Shape<Tile>': area() must give a number
   37 | static_assert(!Shape<Tile>, "area() must give a number");
      |               ^~~~~~~~~~~~
shapes_tests.cpp:38:15: error: static assertion failed due to requirement '!Shape<int>': an int is not a Shape
   38 | static_assert(!Shape<int>, "an int is not a Shape");
      |               ^~~~~~~~~~~
4 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra shapes_tests.cpp -o shapes_tests-gcc && ./shapes_tests-gcc
shapes_tests.cpp:35:15: error: static assertion failed: a type without area() is rejected
   35 | static_assert(!Shape<Label>, "a type without area() is rejected");
      |               ^~~~~~~~~~~~~
shapes_tests.cpp:36:15: error: static assertion failed: a type without name() is rejected
   36 | static_assert(!Shape<Blob>, "a type without name() is rejected");
      |               ^~~~~~~~~~~~
shapes_tests.cpp:37:15: error: static assertion failed: area() must give a number
   37 | static_assert(!Shape<Tile>, "area() must give a number");
      |               ^~~~~~~~~~~~
shapes_tests.cpp:38:15: error: static assertion failed: an int is not a Shape
   38 | static_assert(!Shape<int>, "an int is not a Shape");
      |               ^~~~~~~~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shapes_session.py

The starter’s concept Shape = true; accepts every type, so the four static_assert lines that expect a rejection fail and the program never builds. When both pieces are right, it builds on both compilers and prints 3 of 3 checks passed. Write Shape with two compound requirements on a const T& parameter. Then add a call total_area(std::vector<Label>{}) to the tests and compare the error with the long ones of the median section.

Then test what you can predict without a compiler: which form fits, where an error lands, which requirement stops a type, which overload wins. Your weakest idea comes up first, and each answer you pick is explained against the programs of this lesson:

Practice that adapts to you

Practise this lesson

13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 Which of these make f a template that only accepts integral types, so that f(2.5) is rejected at the call?

    Choose every answer that is right.

    Show the answer: Question 1 of 13

    Answer:

    • void f(std::integral auto x);
    • template <std::integral T> void f(T x);
    • template <typename T> requires std::integral<T> void f(T x);

    The first three are three of the four forms of a constraint, and the compiler checks them during overload resolution, so f(2.5) finds no viable candidate. A static_assert in the body is not a constraint: the function template stays a candidate for every type and fails only once it is instantiated, inside its body.

  2. Question 2 of 13 Box<T> declares void print() const requires Printable<T>;, where Printable needs os << t. A program makes a Box<Point>, and Point has no <<. What happens?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: Box<Point> compiles and works; only a call of print() on it is an error

    Constraining a member with the class's own T is the job of the trailing form. The class template stays usable for every T, and each constrained member exists only for the types that satisfy its condition.

  3. Question 3 of 13 Which of these requires-clauses compiles?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: requires Number<T> && (!std::same_as<T, bool>)

    After requires, each part joined by && or || must be a primary expression: a name such as a concept or a trait's _v, true or false, a requires-expression, or anything in parentheses. Inside a concept's definition the rule does not apply: NumberNotBool writes Number<T> && !std::same_as<T, bool> without parentheses.

  4. Question 4 of 13 Reading has one double member and no operators. Which operation did Clang report missing first when median required std::totally_ordered?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: ==, because totally_ordered builds on equality_comparable

    std::totally_ordered requires equality_comparable first and then the four ordering comparisons, so the chain of notes ends at the expression __t == __u. A standard concept asks for a meaningful set of operations, which can be more than one function body uses.

  5. Question 5 of 13 Where does a template's requirement on T live once the template is constrained with a concept?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: In its declaration, where the compiler checks it at every call

    A concept moves the requirement from the body to the declaration. That is why the error moves too, from the library's helpers to the call in your own code.

  6. Question 6 of 13 Inside a requires-expression, what does the line std::is_trivially_copyable_v<typename T::value_type>; require?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: Only that the expression is valid, whatever its value

    Without requires in front, the line is a simple requirement, which checks only that the expression compiles. A trait that names a valid type always compiles, so std::vector<std::string> passed AlmostTrivialBuffer. A nested requirement, requires std::is_trivially_copyable_v<...>;, checks that the value is true.

  7. Question 7 of 13 Why does std::vector<bool> fail TrivialBuffer while std::vector<int> passes?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: Its const operator[] returns a value, never a const bool&, so the same_as check fails

    A compound requirement with std::same_as checks the exact type of an expression. std::vector<bool> is the one vector whose operator[] gives a value instead of a reference to an element, and the concept catches it with both standard libraries.

  8. Question 8 of 13 With the three describe overloads of subsume.cpp (integral, signed_integral and floating_point), what does describe(42u) print?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: "an integer"

    42u is an unsigned int, which satisfies std::integral but not std::signed_integral, so only the first overload is viable. For 42, an int, both integer overloads are viable and the more constrained signed_integral wins.

  9. Question 9 of 13 A fourth overload is added to subsume.cpp: void describe(std::same_as<int> auto). What does describe(42) do now?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: It is ambiguous, because neither same_as<int> nor signed_integral subsumes the other

    Subsumption orders constraints only when one includes the other. Two concepts built independently are unordered, so a call that satisfies both is ambiguous, however natural the intended choice looks.

  10. Question 10 of 13 One show requires std::is_integral_v<T>, another requires std::is_integral_v<T> && std::is_signed_v<T>. Why is show(42) ambiguous?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: The two is_integral_v<T> are separate atomic constraints, so neither requires-clause subsumes the other

    Atomic constraints count as identical only when they come from the same appearance of the same expression. Writing the trait twice gives two different constraints, so the compiler cannot see that one clause refines the other. Naming the shared part as a concept and building the stricter concept from it fixes the call.

  11. Question 11 of 13 In C++23, one f requires (std::integral<T> && ...) and another requires (std::integral<T> && ...) && (std::is_signed_v<T> && ...). What does f(1, 2) do?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: It is ambiguous, because the integral fold is written twice and so forms two different atoms

    The rule of this section works on packs too: write the shared fold once, as a concept such as AllIntegral, and build the stricter concept from it, as fold_named.cpp does. Then f(1, 2) prints all signed in C++23 on both compilers.

  12. Question 12 of 13 According to the concept table the program printed, which types satisfy std::ranges::random_access_range?

    Choose every answer that is right.

    Show the answer: Question 12 of 13

    Answer:

    • std::string
    • std::vector<int>

    A string and a vector can jump to any element in constant time, so their iterators are random access. A list is a range, but its iterators only move one step at a time, which is why the constrained std::ranges::sort refuses it. An int is not a range at all.

  13. Question 13 of 13 The concept table printed ordered: yes for the type of a lambda without captures. What does that show?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Only that every comparison std::totally_ordered asks for compiles, through the conversion to a function pointer

    A type satisfies a concept when the required expressions compile. Whether those expressions mean anything is a semantic requirement, which the compiler cannot check and the programmer has to.

Interview questions

Fresher · What is the difference between a requires-clause and a requires-expression?

A requires-clause attaches a constraint to a declaration: template <typename T> requires std::integral<T> makes the template a candidate only for types that satisfy it. Any constant boolean expression can follow requires, usually concepts joined with && and ||, and a negation or a comparison among them goes in parentheses. A requires-expression is an expression of type bool that defines a requirement: requires (const T& c) { c.size(); typename T::value_type; } is true when every listed requirement holds, and its body is checked but never run. The two meet in two places: a concept is usually defined as a requires-expression, and a requires-expression may follow a requires-clause directly, which gives requires requires. A good answer adds that the expression only checks that operations compile, so a trait written inside it needs requires in front to be tested for truth.

A follow-up usually sets the two kinds of requires side by side, in code you have not seen. Decide these two:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 What does template <typename T> requires requires (const T& t) { t.size(); } std::size_t f(const T& x); require of T?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: That t.size() compiles for a const T; the first requires starts the clause, the second the expression

    The requires-clause attaches a condition, and here the condition is a requires-expression with one simple requirement. A constraint worth writing once is usually worth naming as a concept, such as HasSize.

  2. Question 2 of 2 template <typename T> concept Small = requires { sizeof(T) <= 8; }; What is Small<std::string>?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: true, because the line is a simple requirement that only has to compile

    Inside a requires-expression a plain expression is a simple requirement: valid is enough. Write requires in front, a nested requirement, when a condition must be true.

Two harder questions take concepts into a codebase you own: an overload set that turned ambiguous after a change, and the choice of a constraint for a widely used template:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A library has encode(T) constrained by requires std::is_integral_v<T>. A colleague adds a faster overload for unsigned types, constrained by requires std::is_integral_v<T> && std::is_unsigned_v<T>, and now every call with an unsigned int fails as ambiguous. Explain why, and fix it without changing any caller.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    You own a generic median(std::vector<T>) used across a large codebase, and reviewers ask you to constrain it. Would you use std::totally_ordered, std::sortable or a concept of your own? Say what each choice costs the callers.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A concept is a named, compile-time true-or-false question about types, and it can constrain a template in four places: instead of typename, in a requires-clause, in a trailing requires-clause, or as Concept auto.
  • With a constraint, a wrong type fails at the call with one error that names the requirement: 19 lines from Clang and 64 from GCC 16 for median, against 334 and 1,457 lines without one.
  • A requires-expression lists simple, type, compound and nested requirements, checked in order until one fails, and a trait written without requires in front is only checked for compiling, never for being true.
  • When two constrained overloads are viable and otherwise equal, the more constrained one wins, as signed_integral wins over integral for describe(42).
  • Subsumption sees only atomic constraints written once, so families of concepts are built from each other rather than by repeating a trait; folds named as concepts compare in C++23, and C++26 (P2963R3, in Clang, not yet in GCC 16) compares the folds themselves.
  • std::integral includes bool and char, a captureless lambda satisfies std::totally_ordered, and double models it despite NaN: concepts check that code compiles, not that its values make sense.

Terms in this lesson

atomic constraint
One of the smallest pieces a constraint splits into at && and ||, such as std::is_signed_v<T>. Two atomic constraints count as the same only when they come from the same expression written in one place, normally inside one concept.
concept
A named compile-time test of template arguments, such as std::integral<T>, defined as template <typename T> concept Name = condition;. Used in a template's declaration, it makes the template accept only the types that satisfy it, and a type that fails is reported at the call.
requires-clause
The word requires and a condition attached to a template, as in template <typename T> requires std::integral<T>, or after a function's parameters. The template takes part in overload resolution only for arguments that satisfy the condition.
requires-expression
An expression of type bool, requires (parameters) { requirements }, that is true when every requirement holds: expressions that must compile, types that must exist, result types to check and nested conditions that must be true. Its body is checked, never run.
semantic requirement
A condition a concept states in words that no compiler checks, such as < being a strict ordering for std::totally_ordered. A type satisfies a concept when the required expressions compile; it models the concept only if it meets these conditions too.
subsumption
How the compiler orders two constrained overloads that are otherwise equally good: a constraint subsumes another when it contains each of the other's atomic constraints, as std::signed_integral<T> contains std::integral<T> and adds one, and the overload whose constraint subsumes is chosen.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.