C++ by Example
C++ by Example

Concepts (C++20)

2 min read

Concepts (C++20)

Concepts constrain template parameters, providing clear compile-time errors and enabling function overloading on type properties. Before concepts, a template that received the wrong type produced a wall of cryptic error messages deep inside the template instantiation. With concepts, the error points directly to the call site.

The problem without concepts

template <typename T>
T max_of(T a, T b) { return (a > b) ? a : b; }

max_of("hello", "world");   // works but compares pointers, not content

The template accepts anything with > — including types where the behavior is wrong.

Defining a concept

A concept is a named boolean predicate on types:

#include <concepts>
#include <iostream>

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

template <Numeric T>
T max_of(T a, T b) { return (a > b) ? a : b; }

int main() {
    std::cout << max_of(3, 7)      << "\n";   // OK
    std::cout << max_of(1.5, 2.5)  << "\n";   // OK
    // max_of("a", "b");   // compile error: const char* does not satisfy Numeric
}

Standard library concepts

<concepts> provides a large set of standard concepts:

Concept Meaning
std::integral<T> T is an integer type
std::floating_point<T> T is a floating-point type
std::same_as<T, U> T and U are the same type
std::derived_from<T, B> T is derived from B
std::convertible_to<T, U> T is convertible to U
std::equality_comparable<T> T supports == and !=
std::totally_ordered<T> T supports all six comparisons
std::copy_constructible<T> T can be copy-constructed
std::invocable<F, Args...> F can be called with Args

requires expressions

For custom constraints, requires describes what operations a type must support:

template <typename T>
concept Printable = requires(T x) {
    { std::cout << x };   // must support stream insertion
};

template <Printable T>
void print(T value) {
    std::cout << value << "\n";
}

print(42);          // OK
print("hello");     // OK
// print(std::mutex{});  // error: std::mutex is not Printable

Concept-based overloading

Concepts enable overloading based on type properties:

#include <concepts>
#include <iostream>

template <std::integral T>
void describe(T x) {
    std::cout << x << " is an integer\n";
}

template <std::floating_point T>
void describe(T x) {
    std::cout << x << " is a float\n";
}

int main() {
    describe(42);    // 42 is an integer
    describe(3.14);  // 3.14 is a float
}

requires clause vs. concept syntax

Multiple ways to express the same constraint:

// 1. Named concept in template parameter
template <std::integral T>
T halve(T x) { return x / 2; }

// 2. requires clause after parameters
template <typename T>
    requires std::integral<T>
T halve(T x) { return x / 2; }

// 3. constrained auto (abbreviated template)
auto halve(std::integral auto x) { return x / 2; }

All three are equivalent. Abbreviated template syntax (option 3) is the most concise for simple cases.

C++ by Example
C++ by Example

Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.

View book →