Concepts

Concepts (C++20) state what a template needs from its type arguments: "a number", "something sortable", "a type with a size() member". They make template code self-documenting, let overloads choose by capability, and turn pages of cryptic template errors into one clear message.

Using standard concepts

The <concepts> header provides many ready-made concepts. Put one in place of typename to constrain a parameter:

C++
#include <concepts>
#include <iostream>

template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        T r = a % b;
        a = b;
        b = r;
    }
    return a;
}

int main() {
    std::cout << gcd(1071, 462) << ' ' << gcd(48L, 18L) << '\n';
}
Output
21 6

Calling gcd(2.5, 1.5) is now rejected immediately, with a message that names the unmet requirement:

C++
#include <concepts>

template <std::integral T>
T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); }

int main() {
    return static_cast<int>(gcd(2.5, 1.5));
}
Compiler error
main.cpp:7:29: error: no matching function for call to 'gcd'
ConceptSatisfied by
std::integral, std::floating_pointInteger / floating-point types
std::same_as<T, U>, std::convertible_to<T, U>Type relationships
std::equality_comparable, std::totally_orderedTypes with == / with < and friends
std::invocable<F, Args...>, std::predicateCallables
std::ranges::range, std::ranges::random_access_rangeThings you can iterate

Three ways to write constraints

C++
#include <concepts>
#include <iostream>

template <std::floating_point T>                     // 1. in the template header
T half(T x) { return x / 2; }

template <typename T>
    requires std::integral<T>                        // 2. a requires-clause
T twice(T x) { return x * 2; }

auto negate(std::signed_integral auto x) {           // 3. constrained auto parameter
    return -x;
}

int main() {
    std::cout << half(5.0) << ' ' << twice(21) << ' ' << negate(7) << '\n';
}
Output
2.5 42 -7

Defining your own concepts

A requires-expression lists operations that must compile for the type:

C++
#include <concepts>
#include <iostream>
#include <string>
#include <vector>

template <typename T>
concept HasArea = requires(const T& shape) {
    { shape.area() } -> std::convertible_to<double>;
};

struct Square { double side; double area() const { return side * side; } };
struct Circle { double r; double area() const { return 3.14159 * r * r; } };

template <HasArea... Shapes>
double total_area(const Shapes&... shapes) {
    return (shapes.area() + ...);          // a fold expression over all arguments
}

int main() {
    std::cout << total_area(Square{2}, Circle{1}, Square{1}) << '\n';
}
Output
8.14159

Unlike the abstract classes of lesson 38, Square and Circle don't inherit from anything: any type with a suitable area() qualifies, and there are no virtual calls. (Shapes... is a parameter pack — a template taking any number of arguments.)

Overloading on concepts

When several constrained overloads match, the compiler picks the most specific one — a clean replacement for older tricks like enable_if:

C++
#include <concepts>
#include <iostream>
#include <string>

template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

void describe(Numeric auto x)      { std::cout << x << " is a number\n"; }
void describe(std::integral auto x) { std::cout << x << " is a whole number\n"; }
void describe(const std::string& s) { std::cout << '"' << s << "\" is text\n"; }

int main() {
    describe(3.5);
    describe(42);              // both match; std::integral is more specific
    describe(std::string{"hi"});
}
Output
3.5 is a number
42 is a whole number
"hi" is text

Clear error messages

Here's what happens when a type doesn't meet a custom concept — the compiler names the concept and the failing requirement:

C++
#include <concepts>

template <typename T>
concept HasArea = requires(const T& s) { { s.area() } -> std::convertible_to<double>; };

double report(const HasArea auto& shape) { return shape.area(); }

struct Line { double length; };

int main() {
    return static_cast<int>(report(Line{3.0}));
}
Compiler error
main.cpp:11:29: error: no matching function for call to 'report'

Try it yourself

Write a concept Container that requires size() and begin()/end(), and a function that prints the size of anything satisfying it.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
container with 3 elements
container with 5 elements
container with 2 elements