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.
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:
// 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>);
} 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:
- The concept in place of
typename:template <Number T>. This is the shortest form when a constraint names one concept of one parameter. - 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 followrequires, 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>). - 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>;makesprintexist only for element types that can be printed. - A constrained
autoparameter: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, eachNumber autoinvents 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:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| 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
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_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);
} 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:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- 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.
- 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 <.
- 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.
- 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'.
- 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.
- 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.
- 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.
Checkpoint
Check: The error at the call
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:
// 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>>);
} 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, sostd::same_as<const typename T::value_type&>checks the exact type ofcbuf[i]. Writingnoexceptafter 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:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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.
// 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
} 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:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
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.
- Deduction gives T = int for every candidate.
- 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.
- 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.
- 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".
- 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
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:
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);
} 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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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):
// 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);
} 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.
// 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
} 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
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:
// 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)");
} 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
Practise: a Shape concept
shapes.h holds a starter for two pieces of a small geometry library:
Shape: a type is a shape when, on aconstobject,area()gives something that converts todoubleandname()gives something that converts tostd::string_view.total_area(shapes): the sum of the areas of astd::vectorof 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:
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;
} 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.
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
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.
-
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.
-
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.
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 asConcept 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
requiresin 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_integralwins overintegralfordescribe(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::integralincludesboolandchar, a captureless lambda satisfiesstd::totally_ordered, anddoublemodels 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
- Working Draft, Programming Languages: C++ (N4950), 7.5.7 Requires expressions, 13.1 Preamble, 13.5 Template constraints, 16.3.2.3 Requirements, 16.4.5.11 Semantic requirements, 18 Concepts library (ISO/IEC JTC1/SC22/WG21)
- P0734R0: Wording Paper, C++ extensions for Concepts (ISO/IEC JTC1/SC22/WG21)
- P0898R3: Standard Library Concepts (ISO/IEC JTC1/SC22/WG21)
- P1141R2: Yet another approach for constrained declarations (ISO/IEC JTC1/SC22/WG21)
- P2963R3: Ordering of constraints involving fold expressions (ISO/IEC JTC1/SC22/WG21)
- Options Controlling C++ Dialect (-fconcepts-diagnostics-depth) (GNU Project)
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Project)
- C++ Standards Support in GCC (GNU Project)
- C++ Support in Clang (LLVM Project)
- Clang Compiler User's Manual (-ferror-limit) (LLVM Project)
- Writing ARM64 code for Apple platforms (Apple)
- Procedure Call Standard for the Arm 64-bit Architecture (AArch64) (Arm)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress