Constraints and concepts (since C++20)
Шаблоны классов , шаблоны функций (включая обобщенные лямбды ), и другие шаблонные функции (обычно члены шаблонов классов) могут быть связаны с ограничением , которое определяет требования к аргументам шаблона, что может использоваться для выбора наиболее подходящих перегрузок функций и специализаций шаблонов.
Именованные наборы таких требований называются концепциями . Каждая концепция является предикатом, вычисляемым во время компиляции, и становится частью интерфейса шаблона, где она используется как ограничение:
#include <cstddef> #include <concepts> #include <functional> #include <string> // Объявление концепта "Hashable", который удовлетворяется любым типом "T" // таким, что для значений "a" типа "T", выражение std::hash<T>{}(a) // компилируется и его результат преобразуется в std::size_t template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; struct meow {}; // Ограниченный шаблон функции C++20: template<Hashable T> void f(T) {} // // Альтернативные способы применения того же ограничения: // template<typename T> // requires Hashable<T> // void f(T) {} // // template<typename T> // void f(T) requires Hashable<T> {} // // void f(Hashable auto /* parameter-name */) {} int main() { using std::operator""s; f("abc"s); // OK, std::string удовлетворяет Hashable // f(meow{}); // Ошибка: meow не удовлетворяет Hashable }
Нарушения ограничений обнаруживаются на этапе компиляции, в начале процесса инстанцирования шаблона, что приводит к понятным сообщениям об ошибках:
std::list<int> l = {3, -1, 10}; std::sort(l.begin(), l.end()); // Типичная диагностика компилятора без концептов: // invalid operands to binary expression ('std::_List_iterator<int>' and // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 строк вывода ... // // Типичная диагностика компилятора с концептами: // error: cannot call std::sort with std::_List_iterator<int> // note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
Цель концепций — моделировать семантические категории (Number, Range, RegularFunction), а не синтаксические ограничения (HasPlus, Array). Согласно ISO C++ core guideline T.20 , «Возможность определять содержательную семантику является определяющей характеристикой истинного концепта в отличие от синтаксического ограничения».
Содержание |
Концепции
Концепт — это именованный набор требований . Определение концепта должно находиться в области видимости пространства имён.
Определение концепции имеет вид
template <
список-параметров-шаблона
>
|
|||||||||
| attr | - | последовательность любого количества attributes |
// концепт template<class T, class U> concept Derived = std::is_base_of<U, T>::value;
Концепты не могут рекурсивно ссылаться на себя и не могут быть ограничены:
template<typename T> concept V = V<T*>; // ошибка: рекурсивный концепт template<class T> concept C1 = true; template<C1 T> concept Error1 = true; // Ошибка: C1 T пытается ограничить определение концепта template<class T> requires C1<T> concept Error2 = true; // Ошибка: requires-выражение пытается ограничить концепт
Явные инстанцирования, явные специализации или частичные специализации концепций не допускаются (смысл исходного определения ограничения не может быть изменён).
Концепты могут быть именованы в id-выражении. Значение id-выражения равно true если constraint-выражение удовлетворено, и false в противном случае.
Концепты также могут быть именованы в type-constraint, как часть
В type-constraint концепт принимает на один шаблонный аргумент меньше, чем требует его список параметров, поскольку контекстно выведенный тип неявно используется в качестве первого аргумента концепта.
template<class T, class U> concept Derived = std::is_base_of<U, T>::value; template<Derived<Base> T> void f(T); // T ограничен концепцией Derived<T, Base>
Ограничения
Ограничение - это последовательность логических операций и операндов, которая задаёт требования к аргументам шаблона. Они могут появляться внутри requires выражений или непосредственно в качестве тел концептов.
Существует три (до C++26) четыре (начиная с C++26) типа ограничений:
|
4)
свертка развернутых ограничений
|
(since C++26) |
Ограничение, связанное с объявлением, определяется путём нормализации логического выражения AND, операнды которого располагаются в следующем порядке:
- ограничивающее выражение, введённое для каждого ограниченного параметра шаблона типа или постоянного параметра шаблона, объявленного с ограниченным типом-заполнителем , в порядке их появления;
- ограничивающее выражение в requires clause после списка параметров шаблона;
- ограничивающее выражение, введённое для каждого параметра с ограниченным типом-заполнителем в объявлении сокращённого шаблона функции ;
- ограничивающее выражение в завершающей requires clause .
Этот порядок определяет последовательность, в которой ограничения инстанцируются при проверке на удовлетворение.
Повторные объявления
Ограниченное объявление может быть переобъявлено только с использованием той же синтаксической формы. Диагностика не требуется:
// Эти первые два объявления f корректны template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // OK, переобъявление // Включение этого третьего, логически-эквивалентного-но-синтаксически-отличного // объявления f некорректно, диагностика не требуется template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // Следующие два объявления имеют различные ограничения: // первое объявление имеет Incrementable<T> && Decrementable<T> // второе объявление имеет Decrementable<T> && Incrementable<T> // Даже несмотря на то, что они логически эквивалентны. template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // некорректно, диагностика не требуется
Союзы
Конъюнкция двух ограничений формируется с использованием оператора
&&
в выражении ограничения:
template<class T> concept Integral = std::is_integral<T>::value; template<class T> concept SignedIntegral = Integral<T> && std::is_signed<T>::value; template<class T> concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
Конъюнкция двух ограничений удовлетворяется только в том случае, если оба ограничения удовлетворены. Конъюнкции вычисляются слева направо с использованием короткого замыкания (если левое ограничение не удовлетворено, подстановка аргументов шаблона в правое ограничение не выполняется: это предотвращает сбои из-за подстановки вне непосредственного контекста).
template<typename T> constexpr bool get_value() { return T::value; } template<typename T> requires (sizeof(T) > 1 && get_value<T>()) void f(T); // #1 void f(int); // #2 void g() { f('A'); // OK, вызывает #2. При проверке ограничений #1, // 'sizeof(char) > 1' не выполняется, поэтому get_value<T>() не проверяется }
Дизъюнкции
Дизъюнкция двух ограничений формируется с использованием оператора
||
в выражении ограничения.
Дизъюнкция двух ограничений удовлетворяется, если удовлетворяется любое из ограничений. Дизъюнкции вычисляются слева направо с использованием сокращённого вычисления (если левое ограничение удовлетворено, подстановка аргументов шаблона в правое ограничение не производится).
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
Атомарные ограничения
Атомарное ограничение состоит из выражения E и отображения из параметров шаблона, которые появляются внутри E в аргументы шаблона, включающие параметры шаблона ограничиваемой сущности, называемого его отображением параметров .
Атомарные ограничения формируются в процессе нормализации ограничений . E никогда не является выражением логического И или логического ИЛИ (они формируют конъюнкции и дизъюнкции соответственно).
Удовлетворение атомарного ограничения проверяется подстановкой отображения параметров и аргументов шаблона в выражение E . Если подстановка приводит к недопустимому типу или выражению, ограничение не удовлетворено. В противном случае E , после любого преобразования lvalue-to-rvalue, должно быть prvalue константным выражением типа bool , и ограничение удовлетворяется тогда и только тогда, когда оно вычисляется в true .
Тип E после подстановки должен быть в точности bool . Никакие преобразования не допускаются:
template<typename T> struct S { constexpr operator bool() const { return true; } }; template<typename T> requires (S<T>{}) void f(T); // #1 void f(int); // #2 void g() { f(0); // ошибка: S<int>{} не имеет тип bool при проверке #1, // несмотря на то что #2 является лучшим соответствием }
Два атомарных ограничения считаются идентичными если они образованы из одного и того же выражения на уровне исходного кода и их отображения параметров эквивалентны.
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_cat = true; template<class T> concept Meowable = is_meowable<T>; template<class T> concept BadMeowableCat = is_meowable<T> && is_cat<T>; template<class T> concept GoodMeowableCat = Meowable<T> && is_cat<T>; template<Meowable T> void f1(T); // #1 template<BadMeowableCat T> void f1(T); // #2 template<Meowable T> void f2(T); // #3 template<GoodMeowableCat T> void f2(T); // #4 void g() { f1(0); // ошибка, неоднозначно: // is_meowable<T> в Meowable и BadMeowableCat образуют различные атомарные // ограничения, которые не идентичны (и поэтому не подчиняются друг другу) f2(0); // OK, вызывает #4, более ограниченный чем #3 // GoodMeowableCat получил свой is_meowable<T> из Meowable }
Свернутые развернутые ограничения
Свернутое развернутое ограничение
формируется из ограничения
Пусть N будет количеством элементов в параметрах развертки пакета:
template <class T> concept A = std::is_move_constructible_v<T>; template <class T> concept B = std::is_copy_constructible_v<T>; template <class T> concept C = A<T> && B<T>; // в C++23 эти две перегрузки g() имеют различные атомарные ограничения // которые не идентичны и поэтому не подчиняют друг друга: вызовы g() неоднозначны // в C++26 свертки развертываются и ограничение на перегрузку #2 (требуется и перемещение, и копирование) // подчиняет ограничение на перегрузку #1 (требуется только перемещение) template <class... T> requires (A<T> && ...) void g(T...); // #1 template <class... T> requires (C<T> && ...) void g(T...); // #2
|
(начиная с C++26) |
Нормализация ограничений
Нормализация ограничений — это процесс, преобразующий выражение ограничения в последовательность конъюнкций и дизъюнкций атомарных ограничений. Нормальная форма выражения определяется следующим образом:
- Нормальная форма выражения ( E ) является нормальной формой E .
- Нормальная форма выражения E1 && E2 является конъюнкцией нормальных форм E1 и E2 .
- Нормальная форма выражения E1 || E2 является дизъюнкцией нормальных форм E1 и E2 .
-
Нормальная форма выражения
C
<
A1, A2, ... , AN
>
, где
Cобозначает концепт, является нормальной формой выражения ограниченияCпосле подстановкиA1,A2, ... ,ANвместо соответствующих параметров шаблонаCв отображениях параметров каждого атомарного ограниченияC. Если любая такая подстановка в отображения параметров приводит к недопустимому типу или выражению, программа является некорректной, диагностика не требуется.
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // OK: нормализуется к дизъюнкции // - T::value (с отображением T -> U*) и // - true (с пустым отображением). // Нет недопустимых типов в отображении, даже несмотря на то, // что T::value некорректно для всех указательных типов template<typename V> concept C = B<V&>; // Нормализуется к дизъюнкции // - T::value (с отображением T-> V&*) и // - true (с пустым отображением). // Недопустимый тип V&* сформирован в отображении => некорректно NDR
|
(начиная с C++26) |
-
Нормальной формой любого другого выражения
E
является атомарное ограничение, выражение которого —
E
, а отображение параметров представляет собой тождественное отображение. Это включает все
выражения свертки
, даже те, которые сворачивают операторы
&&или||.
Пользовательские перегрузки
&&
или
||
не влияют на нормализацию ограничений.
requires условия
Ключевое слово requires используется для введения requires условия , которое задает ограничения на аргументы шаблона или на объявление функции.
template<typename T> void f(T&&) requires Eq<T>; // может появляться как последний элемент декларатора функции template<typename T> requires Addable<T> // или сразу после списка параметров шаблона T add(T a, T b) { return a + b; }
В данном случае ключевое слово requires должно сопровождаться некоторым константным выражением (так что возможно написать requires true ), но предполагается, что будет использоваться именованное понятие (как в примере выше), либо конъюнкция/дизъюнкция именованных понятий, либо requires-выражение .
Выражение должно иметь одну из следующих форм:
- Первичное выражение, например Swappable < T > , std:: is_integral < T > :: value , ( std:: is_object_v < Args > && ... ) , или любое выражение в скобках.
-
Последовательность первичных выражений, объединённых оператором
&&. -
Последовательность вышеупомянутых выражений, объединённых оператором
||.
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_purrable() { return true; } template<class T> void f(T) requires is_meowable<T>; // OK template<class T> void g(T) requires is_purrable<T>(); // ошибка, is_purrable<T>() не является первичным выражением template<class T> void h(T) requires (is_purrable<T>()); // OK
Частичное упорядочивание ограничений
Перед любым дальнейшим анализом, ограничения нормализуются путём подстановки тела каждого именованного концепта и каждого requires выражения до тех пор, пока не останется последовательность конъюнкций и дизъюнкций атомарных ограничений.
Ограничение
P
называется
поглощающим
ограничение
Q
если можно доказать, что
P
влечёт
Q
с точностью до идентичности атомарных ограничений в P и Q. (Типы и выражения не анализируются на эквивалентность:
N > 0
не поглощает
N >= 0
).
В частности, сначала
P
преобразуется в дизъюнктивную нормальную форму, а
Q
преобразуется в конъюнктивную нормальную форму.
P
поглощает
Q
тогда и только тогда, когда:
-
каждая дизъюнктивная клауза в дизъюнктивной нормальной форме
Pпоглощает каждую конъюнктивную клаузу в конъюнктивной нормальной формеQ, где -
дизъюнктивная клауза поглощает конъюнктивную клаузу тогда и только тогда, когда существует атомарное ограничение
Uв дизъюнктивной клаузе и атомарное ограничениеVв конъюнктивной клаузе, такие чтоUпоглощаетV; -
атомарное ограничение
Aпоглощает атомарное ограничениеBтогда и только тогда, когда они идентичны согласно правилам, описанным выше .
|
(начиная с C++26) |
Отношение подчинения определяет частичный порядок ограничений, который используется для определения:
- наилучший подходящий кандидат для нешаблонной функции в разрешении перегрузки
- взятие адреса нешаблонной функции из набора перегруженных функций
- наилучшее соответствие для шаблонного параметра-шаблона
- частичное упорядочивание специализаций шаблонов классов
- частичное упорядочивание шаблонов функций
|
Этот раздел не завершён
Причина: обратные ссылки сверху сюда |
Если объявления
D1
и
D2
являются ограниченными и ассоциированные ограничения
D1
поглощают ассоциированные ограничения
D2
(или если
D2
не имеет ограничений), то говорят, что
D1
как минимум так же ограничено
, как
D2
. Если
D1
как минимум так же ограничено, как
D2
, и
D2
не является как минимум так же ограниченным, как
D1
, то
D1
является
более ограниченным
, чем
D2
.
Если выполняются все следующие условия, то нешаблонная функция
F1
является
более ограниченной по частичному порядку
, чем нешаблонная функция
F2
:
- Они имеют одинаковый список параметров , за исключением типов явных параметров объекта (начиная с C++23) .
- Если они являются функциями-членами, обе являются прямыми членами одного и того же класса.
- Если обе являются нестатическими функциями-членами, они имеют одинаковые типы для своих параметров объекта.
-
F1является более ограниченной, чемF2.
template<typename T> concept Decrementable = requires(T t) { --t; }; template<typename T> concept RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIterator включает Decrementable, но не наоборот template<Decrementable T> void f(T); // #1 template<RevIterator T> void f(T); // #2, более ограниченная чем #1 f(0); // int удовлетворяет только Decrementable, выбирается #1 f((int*)0); // int* удовлетворяет обоим ограничениям, выбирается #2 как более ограниченная template<class T> void g(T); // #3 (без ограничений) template<Decrementable T> void g(T); // #4 g(true); // bool не удовлетворяет Decrementable, выбирается #3 g(0); // int удовлетворяет Decrementable, выбирается #4 как более ограниченная template<typename T> concept RevIterator2 = requires(T t) { --t; *t; }; template<Decrementable T> void h(T); // #5 template<RevIterator2 T> void h(T); // #6 h((int*)0); // неоднозначно
Примечания
| Макрос тестирования возможностей | Значение | Стандарт | Возможность |
|---|---|---|---|
__cpp_concepts
|
201907L
|
(C++20) | Ограничения |
202002L
|
(C++20) | Условно тривиальные специальные функции-члены |
Ключевые слова
Отчеты о дефектах
Следующие отчеты об изменениях поведения, влияющие на дефекты, были применены задним числом к ранее опубликованным стандартам C++.
| DR | Applied to | Behavior as published | Correct behavior |
|---|---|---|---|
| CWG 2428 | C++20 | could not apply attributes to concepts | allowed |
| DR | Применяется к | Поведение в опубликованной версии | Корректное поведение |
|---|---|---|---|
| CWG 2428 | C++20 | нельзя было применять атрибуты к концепциям | разрешено |
Смотрите также
| Выражение requires (C++20) | возвращает prvalue-выражение типа bool , описывающее ограничения |