Ограничения и концепты (начиная с C++20)
- На этой странице описывается основная функциональность языка, адаптированная для C++20. Требования к именованным типам, используемые в спецификации стандартной библиотеки, смотрите в именованных требованиях. Версию этой функциональности для Концепты ТС смотрите здесь.
Шаблоны классов, шаблоны функций и нешаблонные функции (обычно элементы шаблонных классов) могут быть связаны с ограничением, которое определяет требования к аргументам шаблона, которые можно использовать для выбора наиболее подходящих перегрузок функций и специализаций шаблона.
Именованные наборы таких требований называются концептами. Каждый концепт является предикатом, оцениваемым во время компиляции и становится частью интерфейса шаблона, где он используется в качестве ограничения:
#include <string> #include <cstddef> #include <concepts> // Объявление концепта "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 /*parameterName*/) {} 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()); // Типичная диагностика компилятора без концептов: // недопустимые операнды для бинарного выражения ('std::_List_iterator<int>' и // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 строк вывода ... // // Типичная диагностика компилятора с концептами: // ошибка: невозможно вызвать std::sort с помощью std::_List_iterator<int> // примечание: концепт RandomAccessIterator<std::_List_iterator<int>> не был удовлетворён
Назначение концептов моделировать семантические категории (Number, Range, RegularFunction), а не синтаксические ограничения (HasPlus, Array). Согласно Основному Руководству ISO C++ T.20, "Возможность указывать осмысленную семантику является определяющей характеристикой истинного концепта, а не синтаксическим ограничением."
Содержание |
[править] Концепты
Концепт это именованный набор требований. Определение концепта должно появляться в области видимости пространства имён.
Определение концепта имеет вид
template < список-параметров-шаблонов >
|
|||||||||
атрибуты | — | последовательность любого количества атрибутов |
// концепт 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 пытается ограничить концепт
Явные экземпляры, явные специализации или частичные специализации концептов не допускаются (значение исходного определения ограничения не может быть изменено).
Концепты могут быть именованы в выражении-идентификаторе. Значение выражения-идентификатора равно true, если удовлетворяется выражение ограничения, и false в противном случае.
Концепты также могут быть именованы в ограничении типа, как часть
В ограничении-типа концепт принимает на один аргумент шаблона меньше, чем требует его список параметров, потому что тип, выведенный из контекста, неявно используется в качестве первого аргумента концепта.
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 или непосредственно как совокупность концептов.
Существует три типа ограничений:
Ограничение, связанное с объявлением, определяется путём нормализации логического выражения И, операнды которого расположены в следующем порядке:
- выражение ограничения, введённое для каждого ограниченного параметра шаблона типа или параметра шаблона не типа, объявленного с ограниченным типом-заполнителем, в порядке появления;
- выражение ограничения в предложении requires после списка параметров шаблона;
- выражение ограничения, введённое для каждого параметра с ограниченным типом заполнителя в сокращённое объявление шаблона функции;
- выражение ограничения в конце предложения requires.
Этот порядок определяет порядок, в котором реализуются ограничения при проверке на соответствие.
[править] Повторное объявление
Объявление с ограничениями может быть повторно объявлено только с использованием той же синтаксической ф��рмы. Диагностика не требуется:
// Эти первые два объявления 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-в-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 }
[править] Нормализация ограничений
Нормализация ограничений это процесс преобразования выражения ограничения в последовательность конъюнкций и дизъюнкций атомарных ограничений. Нормальная форма выражения определяется следующим образом:
- Нормальная форма выражения (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
- Нормальной формой любого другого выражения 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
тогда и только тогда, когда они идентично используют правила, описанные выше.
Отношение подчинения определяет частичный порядок ограничений, который используется для определения:
- лучшего жизнеспособного кандидата на нешаблонную функцию в разрешении перегрузки
- адреса нешаблонной функции в наборе перегрузки
- лучшего совпадения с шаблонным аргументом шаблона
- частичного упорядочивания специализаций шаблонных классов
- частичного порядка шаблонов функций
Этот раздел не завершён Причина: обратные ссылки сверху сюда |
Если объявления D1
и D2
ограничены, а связанные ограничения D1
включают в себя связанные ограничения D2
(или если D2
не имеет ограничений), тогда говорят, что D1
является по крайней мере столь же ограниченным, как и D2
. Если D1
ограничено как минимум так же, как D2
, а D2
не так ограничено, как D1
, то D1
является более ограниченным, чем D2
.
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++:
Номер | Применён | Поведение в стандарте | Корректное поведение |
---|---|---|---|
CWG 2428 | C++20 | нельзя применять атрибуты к концептам | позволено |
[править] Смотрите также
Выражение requires(C++20) | даёт выражение prvalue типа bool, описывающее ограничения |