Constraints and requires expressions

A simple concept asks whether an expression is valid. A precise one also asks what it returns, combines several requirements, and ranks overloads by how constrained they are.

These are the tools a library needs: a range concept says that begin() returns an iterator, a serialisable concept says that a function accepts the type, and a fast path for a stronger concept must win over the general one. Lesson 043 wrote the first concepts; this lesson writes the ones with structure.

A requirement with a return type

A compound requirement is written as { expression } -> concept and checks two things: that the expression compiles and that its result satisfies the concept on the right. The program requires value.size() to exist and to be convertible to std::size_t.

The concept is true for std::string and std::vector and false for a type with a size() that returns a string. The two calls in the program satisfy it, and the printed sizes come through the checked expression.

convertible_to is one of the standard concepts in <concepts>, together with same_as, integral and derived_from. A compound requirement can use any concept there, including one of your own.

#include <concepts> #include <iostream> #include <string> #include <vector> template <typename T> concept has_size = requires(T value) { { value.size() } -> std::convertible_to<std::size_t>; }; template <has_size T> void show_size(const T& value) { std::cout << value.size() << '\n'; } int main() { show_size(std::string{"abcd"}); show_size(std::vector<int>{1, 2, 3}); }

g++ -std=c++23 -Wall -Wextra compound.cpp -o compound
./compound

4
3

Composing concepts

Concepts combine with && and ||. A concept that needs both a size and an index operator can be written as one requires expression with two lines, or as sized && indexed, where each part is a concept of its own.

The program's container concept requires size() and operator[]. std::vector satisfies it and std::string does not, because std::string does not have an operator[] that returns a reference to a char in the same sense.

Composition keeps each concept small and testable. The standard ranges library is built this way: range, sized_range, input_range and the rest are combined from a handful of primitives.

#include <concepts> #include <iostream> #include <vector> template <typename T> concept indexed = requires(T value, std::size_t i) { value[i]; }; template <typename T> concept container = requires(T value) { value.size(); } && indexed<T>; template <container T> void show_first(const T& value) { std::cout << value[0] << ' ' << value.size() << '\n'; } int main() { show_first(std::vector<int>{4, 5, 6}); }

g++ -std=c++23 -Wall -Wextra compose.cpp -o compose
./compose

4 3

The more constrained overload wins

Two overloads can be constrained by nested concepts, one stricter than the other. When both are viable, the compiler picks the one whose constraints imply the other's, which is subsumption, and no ambiguity error is produced.

The program has a general describe() for any type and a stricter one for integral types. The int call takes the stricter version, the double call the general one, and the output shows which body ran.

Subsumption only works when the stricter constraint is written in terms of the weaker one, as std::integral<T> && std::signed_integral<T> is. Two unrelated concepts that both happen to be true do not order the overloads.

#include <concepts> #include <iostream> template <typename T> void describe(T) { std::cout << "general\n"; } template <typename T> requires std::integral<T> void describe(T) { std::cout << "integral\n"; } int main() { describe(3); describe(2.5); }

g++ -std=c++23 -Wall -Wextra subsumption.cpp -o subsumption
./subsumption

integral
general

A constrained class template

A class template can carry a constraint too, written after the template header. The class is then usable only for types that satisfy it, and every member function inherits the requirement.

The program's Box accepts only types that can be printed with <<, and the call prints an int and a string. A type without << would be rejected at the declaration of the variable, not inside print().

The constraint is checked when the class is instantiated, which is earlier than a member function's body would be checked. That is the same benefit as the function concepts: the error points at the use, not at an implementation detail.

#include <concepts> #include <iostream> #include <string> template <typename T> requires requires(T value) { std::cout << value; } class Box { public: explicit Box(T value) : value_(value) {} void print() const { std::cout << value_ << '\n'; } private: T value_; }; int main() { Box<int> number{3}; Box<std::string> text{"hello"}; number.print(); text.print(); }

g++ -std=c++23 -Wall -Wextra class.cpp -o class_concept
./class_concept

3
hello

An unsatisfied class constraint

Declaring a Box for a type that cannot be printed fails at the declaration. The program uses a struct with no operator<<, and the compiler reports that the constraint is not satisfied.

The message names the concept, the type and the requirement inside the requires expression that could not be met. It does not mention print(), because the class itself was rejected before any function was compiled.

That is the difference between constraining the class and constraining each function: the class constraint catches the mistake at the point where the type is chosen, which is usually closer to the person who can fix it.

#include <iostream> struct opaque { int value; }; template <typename T> requires requires(T value) { std::cout << value; } class Box { public: explicit Box(T value) : value_(value) {} private: T value_; }; int main() { Box<opaque> box{opaque{1}}; return 0; }

g++ -std=c++23 -Wall -Wextra unprintable.cpp -o unprintable

unprintable.cpp: In function 'int main()':
unprintable.cpp:21:15: error: template constraint failure for 'template<class T> requires requires(T value) {std::cout << value;} class Box'
21 | Box<opaque> box{opaque{1}};
| ^
unprintable.cpp:21:15: note: constraints not satisfied
unprintable.cpp: In substitution of 'template<class T> requires requires(T value) {std::cout << value;} class Box [with T = opaque]':
unprintable.cpp:21:15: required from here
unprintable.cpp:10:7: required by the constraints of 'template<class T> requires requires(T value) {std::cout << value;} class Box'
unprintable.cpp:9:14: in requirements with 'T value' [with T = opaque]
unprintable.cpp:9:44: note: the required expression '(std::cout << value)' is invalid
9 | requires requires(T value) { std::cout << value; }
| ~~~~~~~~~~^~~~~~~~
cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail
unprintable.cpp:21:21: error: cannot convert 'opaque' to 'int' in initialization
21 | Box<opaque> box{opaque{1}};
| ^~~~~~~~~
| |
| opaque
unprintable.cpp:21:17: warning: unused variable 'box' [-Wunused-variable]
21 | Box<opaque> box{opaque{1}};
| ^~~

Exercises

  1. Add a concept that requires both size() and empty() to compound.cpp and make show_size use it.
  2. Write a third describe() overload constrained by std::floating_point and check which one describe(2.5f) calls.
  3. Remove the requires clause from Box in class.cpp and see how far the error moves.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a compound requirement add to a simple one?

    Answer

    It checks that the expression is valid and has a convertible return type

    The { expression } -> Type form tests both the syntax and the result type.

  2. How are two concepts combined into a stronger one?

    Answer

    With && between the requirements

    The conjunction is satisfied only when both parts are, and it also gives the subsumption rule its order.

  3. What does subsumption mean for overloads?

    Answer

    The more constrained overload is preferred

    A constraint that contains another is more specialised, so it wins without an ambiguity.

  4. How is a class template constrained?

    Answer

    With a requires clause after the template header

    The constraint on the class is checked at the instantiation of the type.

  5. What does this program print? #include <concepts> #include <iostream> struct four { int value() const { return 4; } }; struct three { int value() const { return 3; } }; template <typename T> concept valued = requires(T t) { { t.value() } -> std::convertible_to<int>; }; template <valued T> int read(const T& t) { return t.value(); } int main() { std::cout << read(four{}) << ' ' << read(three{}); }

    Answer

    4 3

    Both types satisfy the compound requirement, and their functions return 4 and 3.

  6. What does this program print? #include <concepts> #include <iostream> template <typename T> concept numeric = std::integral<T> && std::totally_ordered<T>; template <numeric T> T larger(T a, T b) { return a > b ? a : b; } int main() { std::cout << larger(4, 3) << ' ' << larger(2, 3); }

    Answer

    4 3

    The composition admits int, and each call returns the larger of its two values.

  7. What does this program print? #include <concepts> #include <iostream> int show(std::integral auto value) { std::cout << "integral"; return value; } int show(auto value) { std::cout << "general"; return value; } int main() { show(1); std::cout << ' '; show(1.5); }

    Answer

    integral general

    The integral overload is more constrained, so it wins for the int and the general one handles the double.

  8. What does this program print? #include <concepts> #include <iostream> #include <string> template <typename T> requires requires(T value) { std::cout << value; } class box { public: explicit box(T v) : value_(v) {} void show() const { std::cout << value_ << ' '; } private: T value_; }; int main() { box<int> a{3}; box<std::string> b{"hello"}; a.show(); b.show(); }

    Answer

    3 hello

    Both types can be printed, so both instantiations are allowed, and each prints its stored value.

  9. What does Box<opaque> report when opaque cannot be printed?

    Answer

    template constraint failure, with a note that constraints are not satisfied

    The requires expression is false for the type, and the class template cannot be instantiated.

  10. Why write a requires expression instead of a list of type traits?

    Answer

    It states the operations the type must support

    The expression is the requirement in the same form the code will use it.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev

Banner image