Search Knowledge

© 2026 LIBREUNI PROJECT

Modern C++ Programming / Modern C++20/23

Concepts and Constraints (C++20)

Concepts: Constraining Templates

C++20 concepts provide a way to specify requirements on template parameters, replacing complex SFINAE with readable constraints.

Basic Concept Syntax

#include <concepts>

template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

template<Numeric T>
T add(T a, T b) {
    return a + b;
}

add(5, 10);      // OK: int is Numeric
add(3.14, 2.71); // OK: double is Numeric
// add("hello", "world");  // Error: const char* is not Numeric

Standard Library Concepts

#include <concepts>

// Fundamental concepts
std::integral<int>;           // Integer types
std::floating_point<double>;  // Floating-point types
std::signed_integral<int>;    // Signed integers
std::unsigned_integral<size_t>;  // Unsigned integers

// Comparison concepts
std::equality_comparable<std::string>;
std::totally_ordered<int>;  // Has <, >, <=, >=, ==, !=

// Object concepts
std::movable<std::unique_ptr<int>>;
std::copyable<std::string>;
std::semiregular<int>;  // Default constructible and copyable
std::regular<int>;      // Semiregular and equality comparable

Defining Custom Concepts

template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;  // Must support +
};

template<typename T>
concept Container = requires(T t) {
    typename T::value_type;    // Must have value_type
    { t.begin() };              // Must have begin()
    { t.end() };                // Must have end()
    { t.size() } -> std::convertible_to<size_t>;  // size() returns size_t-like
};

template<Container C>
void process(const C& container) {
    for (const auto& elem : container) {
        // ...
    }
}

requires Clauses

Trailing requires

template<typename T>
T max(T a, T b) requires std::totally_ordered<T> {
    return (a > b) ? a : b;
}

Constrained auto

std::integral auto square(std::integral auto x) {
    return x * x;
}

square(5);    // OK
// square(3.14);  // Error: double is not integral

Combining Concepts

template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;

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

template<typename T>
concept Range = requires(T t) {
    t.begin();
    t.end();
};

requires Expression

template<typename T>
concept Drawable = requires(T t) {
    { t.draw() } -> std::same_as<void>;  // Must have void draw()
    { t.position() } -> std::convertible_to<std::pair<int, int>>;
    t.visible;  // Must have visible member
};

template<typename T>
concept Arithmetic = requires(T a, T b) {
    a + b;
    a - b;
    a * b;
    a / b;
};

Nested Requirements

template<typename T>
concept Complex = requires(T t) {
    requires std::regular<T>;  // Must be regular
    requires sizeof(T) >= 8;   // Size requirement
    { t.real() } -> std::floating_point;
    { t.imag() } -> std::floating_point;
};

Subsumption

More constrained concept is preferred:

template<typename T>
concept Integral = std::is_integral_v<T>;

template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;

void process(Integral auto x) {
    std::cout << "Integral\\n";
}

void process(SignedIntegral auto x) {  // More constrained
    std::cout << "Signed integral\\n";
}

process(5);    // Calls SignedIntegral version
process(5u);   // Calls Integral version

Concept-Based Overloading

void print(std::integral auto x) {
    std::cout << "Integer: " << x << '\\n';
}

void print(std::floating_point auto x) {
    std::cout << "Float: " << x << '\\n';
}

print(42);    // Calls integral version
print(3.14);  // Calls floating_point version

Debugging Concept Failures

template<typename T>
concept Sortable = requires(T t) {
    { t.begin() } -> std::random_access_iterator;
    { t.end() } -> std::random_access_iterator;
};

std::list<int> lst;
// std::sort(lst.begin(), lst.end());  
// Clear error: list iterators are not random_access_iterator

Abbreviated Function Templates

// Before concepts
template<typename T>
void func(T x);

// With concepts (C++20)
void func(auto x);  // Unconstrained

void func(std::integral auto x);  // Constrained

// Multiple parameters
void func2(std::integral auto x, std::floating_point auto y);

Practical Example: Generic Algorithm

template<typename Range, typename Pred>
    requires std::ranges::range<Range> && 
             std::predicate<Pred, std::ranges::range_value_t<Range>>
auto filter(Range&& r, Pred p) {
    std::vector<std::ranges::range_value_t<Range>> result;
    for (auto&& elem : r) {
        if (p(elem)) {
            result.push_back(elem);
        }
    }
    return result;
}
Conceptual Check

What advantage do concepts have over SFINAE?

Runtime Environment

Interactive Lab

1#include <iostream>
2#include <concepts>
3#include <vector>
4 
5template<typename T>
6concept Numeric = std::integral<T> || std::floating_point<T>;
7 
8template<Numeric T>
9T multiply(T a, T b) {
10 return a * b;
11}
12 
13// Constrained auto
14auto add(std::integral auto a, std::integral auto b) {
15 return a + b;
16}
17 
18int main() {
19 std::cout << multiply(5, 10) << '\n';
20 std::cout << multiply(3.14, 2.0) << '\n';
21 std::cout << add(100, 200) << '\n';
22
23 // Compile-time check
24 static_assert(Numeric<int>);
25 static_assert(Numeric<double>);
26 static_assert(!Numeric<std::string>);
27
28 std::cout << "All concept checks passed\n";
29
30 return 0;
31}
System Console

Waiting for signal...