Concepts

A concept is a named requirement on a type: integral, floating point, comparable, a range. Writing it in a template tells the compiler what the function needs, and tells a caller what the function accepts before the call is made.

The benefit shows up in the error. An unconstrained template that uses + on a type without + produces a page of notes about a failed instantiation; a constrained one produces one line saying the type does not satisfy the concept. Lesson 042 produced the first kind; this lesson produces the second.

A requires clause

template <typename T> requires std::integral<T> adds a constraint to the template. The function is only a candidate when T is an integral type, and the standard library already defines the concept in <concepts>.

The program sums two ints through the constrained template. The call is ordinary, and the constraint is checked before the body is looked at.

The requires clause can also be written after the parameter list, and both spellings mean the same thing. The form before the return type is easier to read when the constraint is long.

A constraint is not a run-time check. It removes the function from the overload set for types that fail it, so a program that compiles has already proved every call satisfies the template's requirements, and nothing is tested at run time.

#include <concepts> #include <iostream> template <typename T> requires std::integral<T> T add(T a, T b) { return a + b; } int main() { std::cout << add(2, 3) << '\n'; }

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

5

A concept of your own

A concept is a constexpr bool template, usually written with a requires expression. requires(T t) { t.size(); t.empty(); } is true when both expressions are valid for a T, and the concept becomes a readable name for that requirement.

The program's describe takes any type that satisfies sized and prints its size and whether it is empty. std::string and std::vector both qualify; an int would not.

A requires expression only checks that the expressions compile, not what they return. The next lesson adds return-type requirements, which is how a concept says size() must return something convertible to std::size_t.

Concepts compose: sized && std::copyable<T> is a concept too, and the standard library's ranges concepts are built from dozens of small ones like this.

#include <concepts> #include <iostream> #include <string> #include <vector> template <typename T> concept sized = requires(T value) { value.size(); value.empty(); }; template <sized T> void describe(const T& value) { std::cout << value.size() << ' ' << value.empty() << '\n'; } int main() { describe(std::string{"abc"}); describe(std::vector<int>{1, 2}); }

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

3 0
2 0

The abbreviated form

void show(std::integral auto value) is an abbreviated function template: the parameter type writes the concept where the type would be, and the function becomes a template on it. It is the shortest way to constrain a single parameter.

The program prints an int and a long through the same function. The concept allows both, and the compiler deduces the exact type for each call.

The abbreviated form is syntax sugar for a template with a requires clause. Use it for one or two parameters; a named concept on a template header is clearer when the constraint is the point of the function.

The concept in the parameter position also documents the function at the call site: a reader of show() sees that it wants an integral value without opening the template header.

#include <concepts> #include <iostream> void show(std::integral auto value) { std::cout << value << '\n'; } int main() { show(7); show(42L); }

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

7
42

The error a concept produces

Calling a constrained function with a type that does not satisfy the concept is an error at the call, and the message names the call, the candidate with its constraint, and the expression that failed with the type substituted. There is no trace into the body, because the body was never a candidate.

The program calls add with a double. The constraint requires std::integral, double is floating point, and the last note says the expression is_integral_v<double> evaluated to false: the compiler tells you which requirement failed, not which line inside the function broke.

Compare that with the error of an unconstrained template: the compiler would instantiate the body and complain about something inside it, several frames away from the call that caused it. That difference is the main practical reason to write concepts.

#include <concepts> template <typename T> requires std::integral<T> T add(T a, T b) { return a + b; } int main() { return add(1.5, 2.5); }

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

wrong_type.cpp: In function 'int main()':
wrong_type.cpp:12:15: error: no matching function for call to 'add(double, double)'
12 | return add(1.5, 2.5);
| ~~~^~~~~~~~~~
wrong_type.cpp:12:15: note: there is 1 candidate
wrong_type.cpp:5:3: note: candidate 1: 'template<class T> requires integral<T> T add(T, T)'
5 | T add(T a, T b)
| ^~~
wrong_type.cpp:5:3: note: template argument deduction/substitution failed:
wrong_type.cpp:5:3: note: constraints not satisfied
In file included from wrong_type.cpp:1:
/usr/include/c++/15.2.0/concepts: In substitution of 'template<class T> requires integral<T> T add(T, T) [with T = double]':
wrong_type.cpp:12:15: required from here
12 | return add(1.5, 2.5);
| ~~~^~~~~~~~~~
/usr/include/c++/15.2.0/concepts:109:13: required for the satisfaction of 'integral<T>' [with T = double]
/usr/include/c++/15.2.0/concepts:109:24: note: the expression 'is_integral_v<_Tp> [with _Tp = double]' evaluated to 'false'
109 | concept integral = is_integral_v<_Tp>;
| ^~~~~~~~~~~~~~~~~~

Exercises

  1. Write a concept that requires both + and * on a type and use it on a small vector class of your own.
  2. Give describe a second overload for types that satisfy sized but are not copyable, and see which one a std::string picks.
  3. Remove the requires clause from wrong_type.cpp and compare the error with the constrained one.

Questions

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

  1. What is a concept?

    Answer

    A named requirement on a type or a set of types

    The name collects the requirement, and a template can use it in its declaration.

  2. When is a requires clause checked?

    Answer

    At the call, before the function body is instantiated

    The constraint filters the candidates, so a failing type never reaches the instantiation.

  3. What is the main benefit of a concept over an unconstrained template?

    Answer

    The error names the unmet requirement at the call

    Without the concept the error appears deep in the body, in terms of the operations that failed.

  4. What is the abbreviated form of a constrained parameter?

    Answer

    void f(Integral auto value)

    The concept in the parameter type is the abbreviated syntax for a constrained template.

  5. What does this program print? #include <concepts> #include <iostream> template <std::integral T> T add(T a, T b) { return a + b; } int main() { std::cout << add(2, 3); }

    Answer

    5

    The constraint accepts int, and the function returns the sum.

  6. What does this program print? #include <iostream> #include <vector> template <typename T> concept sized = requires(T value) { value.size(); }; template <sized T> std::size_t size_of(const T& value) { return value.size(); } template <typename T> std::size_t size_of(const T&) { return 0; } int main() { std::vector<int> a{1, 2, 3}; std::vector<int> b{1, 2}; std::cout << size_of(a) << ' ' << size_of(7) << " / " << size_of(b) << ' ' << size_of(2.5); }

    Answer

    3 0 / 2 0

    The constrained overload is chosen for the vectors and the general one for the numbers.

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

    Answer

    7 42

    The abbreviated form constrains each call to an integral type, and both values are printed.

  8. What does calling the constrained add with two doubles report?

    Answer

    no matching function for call to 'add(double, double)', with the constrained candidate shown

    The concept removes the candidate, and the note shows the declaration with its requirement.

  9. Why is a concept better than a comment that says what a template needs?

    Answer

    The compiler enforces the requirement

    A comment is not checked, and a concept is part of the type checking.

  10. What is std::integral an example of?

    Answer

    A standard concept

    The standard library ships a set of named concepts, and integral is one of them.

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