Перейти к содержимому

C++20 Концепции и ограничения

Определение точных требований к шаблонным аргументам с использованием концепций. Напишите более чистый общий код с требованиями клаузул, сокращенных шаблонов функций и стандартных библиотечных концепций.

Решаются проблемные концепции

До появления концепций сообщения об ошибках шаблонов были зашифрованы — использование std::sort на типе без operator< могло создавать страницы текста ошибок глубоко внутри реализации стандартной библиотеки. Концепции решают эту проблему, позволяя вам устанавливать требования **вверх по параметрам шаблона.

concept — названный булевый предикат, оцениваемый во время компиляции. Он проверяет, удовлетворяет ли тип набору синтаксических и семантических требований. Когда концепция не удовлетворена, компилятор создает четкое, краткое сообщение об ошибке, указывающее на неудовлетворенное требование, а не на стену шаблонных обратных следов.

Концепции были введены в C++20 и представляют собой наиболее значительное улучшение общего программирования, поскольку сами шаблоны.

Определение концепций

Концепция определяется ключевым словом concept с последующим выражением **, которое определяет, какие операции должен поддерживать тип. Требуемое выражение может проверять допустимые выражения, типы возврата, вложенные типы и многое другое.

concept_definition.cpp
#include <concepts>
#include <iostream>
#include <string>

// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;  // a+b must return something convertible to T
    { a == b } -> std::convertible_to<bool>; // must be comparable
    requires std::copy_constructible<T>;     // nested requirement
};

// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
    { t.size() } -> std::convertible_to<std::size_t>;
    { t.empty() } -> std::convertible_to<bool>;
};

// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
    return a + b + c;
}

template <Sizable Container>
void report_size(const Container& c) {
    std::cout << "Size: " << c.size()
              << (c.empty() ? " (empty)" : "") << '\n';
}

int main() {
    std::cout << sum_three(1, 2, 3)       << '\n'; // 6
    std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0

    std::string s = "hello";
    report_size(s);  // Size: 5
    std::vector<int> v{1, 2, 3};
    report_size(v);  // Size: 3
}

Требуется пункт и сокращенный синтаксис

Существует несколько способов применения концепции к шаблону. обязательный пункт следует за списком параметров шаблона или за подписью функции. Сокращенный синтаксис шаблона функции ** использует Concept auto как тип параметра, который является наиболее краткой формой.

requires_clause.cpp
#include <concepts>
#include <iostream>

// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        T temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// Method 2: requires clause after template parameters
template <typename T>
    requires std::integral<T>
T lcm(T a, T b) {
    return (a / gcd(a, b)) * b;
}

// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
    return value < 0 ? -value : value;
}

// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
    std::cout << "Integral: " << value << '\n';
}

// Concept auto works for return types too (C++20)
std::integral auto compute() {
    return 42;  // must return an integral type
}

int main() {
    std::cout << gcd(12, 8)       << '\n'; // 4
    std::cout << lcm(12, 8)       << '\n'; // 24
    std::cout << absolute(-42)    << '\n'; // 42
    print_integral(100);                    // Integral: 100
    // print_integral(3.14);  // ERROR: constraint not satisfied
}

Состав и стандартные концепции

Концепции состоят из && (соединение) и || (разделение). Стандартная библиотека предоставляет богатый набор концепций в :

Основные языковые понятия:
std::same_as — T и U однотипны
- std::derived_from - Производный от Base
- std::convertible_to - от неявно конвертируемого до
- std::integral, std::floating_point, std::signed_integral

Концепции сравнения:
- std::equality_comparable - поддерживает == и !=
- std::totally_ordered - поддерживает <, >, <=, >=, ==, !=

Концепции объекта:
- std::movable, std::copyable, std::semiregular, std::regular

Кабельные понятия:
- std::invocable - F можно вызвать с помощью Args
- std::predicate - F - предикатный возвратный бул

Итераторы ()
- std::input_iterator, std::forward_iterator, std::random_access_iterator, std::contiguous_iterator

Концепции диапазона ():
- std::ranges::range, std::ranges::sized_range, std::ranges::random_access_range

Концепция решения проблемы потребления и перегрузки

При совпадении нескольких ограниченных перегрузок компилятор использует concept subsumption, чтобы выбрать наиболее конкретный. Понятие А включает понятие В, если ограничения А логически подразумевают ограничения В. Это позволяет «уточнить» — более конкретное понятие побеждает более общее.

subsumption.cpp
#include <concepts>
#include <iostream>

// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
    { os << t };
};

template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;

// Less constrained overload
template <Printable T>
void display(const T& value) {
    std::cout << "[generic] " << value << '\n';
}

// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
    std::cout << "[number] " << value << '\n';
}

int main() {
    display(42);              // [number] 42 — PrintableNumber is more specific
    display(std::string("hi")); // [generic] hi — only Printable matches
    display(3.14);            // [generic] 3.14 — not integral
}

Требуется шаблон

Несколько необычный requires requires появляется, когда вы используете ad-hoc требует выражения непосредственно в требуемом пункте без определения названной концепции. Первый requires вводит ограничительный пункт; второй requires начинает выражение требований.

requires_requires.cpp
#include <iostream>
#include <string>

// Ad-hoc constraint without naming a concept
template <typename T>
    requires requires(T a, T b) {
        { a + b } -> std::same_as<T>;
    }
T add(T a, T b) {
    return a + b;
}

// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template <AddableToSelf T>
T add_named(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2)                << '\n'; // 3
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
    std::cout << add_named(10, 20)        << '\n'; // 30
}
Лучшая практика

При разработке концепций:

Имена понятий после того, что тип IS, а не то, что функция нуждается - используйте Sortable не HasLessThan. Концепции должны охватывать семантические категории.
Перед тем, как писать свой собственный, проверьте, есть ли у , или то, что вам нужно.
Сохраняйте концепции малыми и композитными — объединяйте малые концепции с && вместо создания монолитных требований.
Использовать поименованные концепты над ad-hoc requires requires — названная форма более читаема и многоразовая.
Документальные семантические требования в комментариях — понятия проверяют синтаксис, а не семантику. Если вы хотите, чтобы operator+ был коммутативным, скажите об этом в комментарии.

Ключевые выводы
  • Концепции называются булевыми предикатами, которые ограничивают параметры шаблона, обеспечивая четкие сообщения об ошибках.
  • Экспрессия requires проверяет действительные выражения, типы возврата и вложенные требования.
  • Четыре способа применения концепций: ограничение на парам шаблона, требование оговорки, требование отслеживания или Concept auto
  • Концепции, входящие в состав && и ||; поглощение выбирает наиболее ограниченную перегрузку
  • Стандартная библиотека предоставляет концепции в , и .
  • requires requires - это специальное встроенное ограничение - предпочитайте названные концепции для читаемости

Тест — проверьте свои знания

(20 XP)

1. Что означает `void print(std::integral auto value)`?

2. Если понятие А определяется как `concept A = B<T> && C<T>`, что происходит, когда и B-ограниченный, и A-ограниченный перегрузочный матч?

3. Почему у `requires requires(T a) { a.size(); }` слово «требуется» дважды?