Skip to content

Concepts

Concepts

Concepts (C++20) define named sets of template requirements.

Class templates, function templates, and non-template functions (typically members of class templates) may be associated with a template constraint, which specifies the requirements on template arguments, which can be used to select the most appropriate function overloads and template specializations.

Each concept is a predicate, evaluated at compile time, and becomes a part of the interface of a template where it is used as a constraint:

1
2
add_executable(concepts concepts.cpp)
target_compile_features(concepts PRIVATE cxx_std_20)

1
#include <concepts>

1
2
3
4
template <typename T> concept Number = std::is_arithmetic_v<T>;

// is_arithmetic_v might be misleading since it also includes bool
// is_arithmetic_v might be misleading since it doesn't include std::complex

1
template<typename T> concept NotNumber = !Number<T>;

1
template<typename T> concept SignedNumber = Number<T> && std::is_signed_v<T>;

1
2
template<typename T>
concept UnsignedNumber = Number<T> && !std::is_signed_v<T>;

1
2
template<typename T1, typename T2>
concept Equivalent = std::is_convertible_v<T1, T2> || std::is_same_v<T1, T2>;

1
2
3
4
5
// These expressions must be something that will compile
template<typename T> concept Range = requires(T r) {
    *r.begin();
    *r.end();
};

1
2
3
4
// These expressions must something that will compile
template<typename T> concept HasValueType = requires(T r) {
    typename T::value_type; // required nested name
};

1
2
3
4
5
// Two requirements at the same time
template<typename T> concept Pair = requires(T p) {
    { p.first };
    { p.second };
};

1
2
3
4
5
6
// The expression should not only compile, but it should also have
// a type that passes the concept after ->
template<typename T> concept IntPair = requires(T p) {
    { p.first } -> std::same_as<int>;
    { p.second } -> std::same_as<int>;
};

1
2
3
4
5
template<typename T>
concept EqualityComparable = requires(T a, T b) {
    { a == b };
    { a != b };
};

1
2
3
4
5
// Additional constraint to existing constraints
// Functions can use Range or RangeSameType now without ambiguity
template<typename T> concept PairSameType = Pair<T> && requires(T p) {
    { p.first } -> std::same_as<decltype(p.second)>;
};

1
2
template<typename T> concept RangeWithValueType = Range<T> && HasValueType<T>;
template<typename T> concept RangeWithoutValueType = Range<T> && !HasValueType<T>;

1
2
3
4
5
6
7
8
9
// Number, but nor SignedNumber neither UnsignedNumber
template<Number T>
void max_value(T a, T b) {
    if (a > b) {
        std::cout << "max_value: " << a << '\n';
    } else {
        std::cout << "max_value: " << b << '\n';
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// This function is used when Number is SignedNumber
template <SignedNumber T>
void max_value(T a, T b) {
    if (a > b) {
        if (a > 0) {
            std::cout << "max_value: positive " << a << '\n';
        } else {
            std::cout << "max_value: negative " << -a << '\n';
        }
    } else {
        if (b > 0) {
            std::cout << "max_value: positive " << b << '\n';
        } else {
            std::cout << "max_value: negative " << -b << '\n';
        }
    }
}

1
2
3
4
5
6
7
8
9
// This function is used when Number is UnsignedNumber
template<UnsignedNumber T>
void max_value(T a, T b) {
    if (a > b) {
        std::cout << "max_value: +" << a << '\n';
    } else {
        std::cout << "max_value: +" << b << '\n';
    }
}

1
2
3
4
5
6
// NotNumber == !Number<T>
template<NotNumber T>
void max_value(T a, T b) {
    std::cout << "a: " << a << '\n';
    std::cout << "b: " << b << '\n';
}

1
2
3
4
template <class T>
void print_element(const T &c) {
    std::cout << c << '\n';
}

1
2
3
4
5
6
7
8
// - The most constrained concept is always used
template<Range T>
void print_element(const T &c) {
    for (const auto &item : c) {
        std::cout << item << ' ';
    }
    std::cout << '\n';
}

1
2
3
4
5
6
7
8
9
// The most constrained concept is used in nested constraints
template<RangeWithValueType T>
void print_element(const T &c) {
    std::cout << typeid(typename T::value_type).name() << ": ";
    for (const auto &item : c) {
        std::cout << item << ' ';
    }
    std::cout << '\n';
}

1
2
3
4
5
6
7
8
9
template<typename T>
requires EqualityComparable<T>
void compare(const T &a, const T &b) {
    if (a == b) {
        std::cout << a << " == " << b << " is " << (a == b ? "true" : "false") << '\n';
    } else {
        std::cout << a << " != " << b << " is " << (a != b ? "true" : "false") << '\n';
    }
}

1
2
3
4
template<typename T>
void compare(const T &a, const T &b) {
    std::cout << typeid(T).name() << ": " << a.c << " == " << b.c << "?" << '\n';
}

1
2
3
4
5
template<Pair T>
void print_pair(const T &p) {
    std::cout << typeid(p.first).name() << ": " << p.first << ", " << typeid(p.second).name() << ": " << p.second
              << '\n';
}

1
2
3
4
template<PairSameType T>
void print_pair(const T &p) {
    std::cout << typeid(p.first).name() << ": " << p.first << ", " << p.second << '\n';
}

Share Snippets