Ограничения и концепты
На этой странице описана экспериментальная функциональность ядра языка. Требования к именованию типов, используемые в спецификации стандартной библиотеки, смотрите в именованных требованиях
Шаблоны классов, шаблоны функций и нешаблонные функции (обычно элементы шаблонов классов) могут быть связаны с ограничением, которое определяет требования к аргументам шаблона, которые можно использовать для выбора наиболее подходящих перегрузок функций и специализаций шаблона.
Ограничения также могут использоваться для ограничения автоматического вывода типов в объявлениях переменных и возвращаемых типов функций только теми типами, которые соответствуют указанным требованиям.
Именованные наборы таких требований называются концептами. Каждый концепт является предикатом, оценивается во время компиляции и становится частью интерфейса шаблона, где используется как ограничение:
#include <string>#include <locale>usingnamespace std::literals; // Объявление концепта "EqualityComparable", которому соответствует// любой тип T, такой, что для значений a и b типа T,// выражение a==b компилируется, и его результат может быть преобразован в booltemplate<typename T> concept bool EqualityComparable = requires(T a, T b){{ a == b }->bool;}; void f(EqualityComparable&&);// объявление ограниченного шаблона функции// template<typename T>// void f(T&&) requires EqualityComparable<T>; // длинная форма того же int main(){ f("abc"s);// OK, std::string соответствует EqualityComparable f(std::use_facet<std::ctype<char>>(std::locale{}));// Ошибка: не EqualityComparable }
Нарушения ограничений обнаруживаются во время компиляции, на ранней стадии процесса создания экземпляра шаблона, что приводит к появлению легко отслеживаемых сообщений об ошибках.
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++ T.20, "Возможность указать осмысленную семантику это определение характеристики истинности концепта, в отличие от синтаксических ограничений."
Если тестирование функциональности поддерживается, описанные здесь функциональные возможности обозначаются макроконстантой __cpp_concepts со значением, равным или большим 201507.
Содержание |
[править]Заполнители
Неограниченный заполнитель auto и ограниченные заполнители, которые имеют форму имя-концепта<
список-аргументов-шаблона(optional)>
, являются заполнителями для выводимого типа.
Заполнители могут появляться в объявлениях переменных (в этом случае они выводятся из инициализатора) или в типах возвращаемых функциями (в этом случае они выводятся из операторов возврата)
std::pair<auto, auto> p2 =std::make_pair(0, 'a');// первое auto это int,// второе auto это char Sortable x = f(y);// тип x выводится из возвращаемого типа f, компилируется только// в том случае, если тип соответствует ограничению Sortable auto f(Container)-> Sortable;// тип возвращаемого значения выводится// из оператора return, компилируется только// в том случае, если тип соответствует Sortable
Заполнители также могут появляться в параметрах, и в этом случае они превращают объявления функций в объявления шаблонов (ограниченных, если заполнитель ограничен)
void f(std::pair<auto, EqualityComparable>);// это шаблон с двумя параметрами:// параметр неограниченного типа и ограниченный параметр без типа
Заполнители с ограничениями можно использовать везде, где можно использовать auto, например, в общих лямбда-объявлениях.
auto gl =[](Assignable& a, auto* b){ a =*b;};
Если спецификатор ограниченного типа обозначает не тип или шаблон, но используется в качестве ограниченного заполнителя, программа неправильно сформирована:
template<size_t N> concept bool Even =(N%2==0);struct S1 {int n;};int Even::* p2 =&S1::n;// ошибка, недопустимое использование концепта,// не связанного с типомvoid f(std::array<auto, Even>);// ошибка, недопустимое использование концепта,// не связанного с типомtemplate<Even N>void f(std::array<auto, N>);// OK
[править]Сокращённые шаблоны
Если в списке параметров функции появляется один или несколько заполнителей, объявление функции на самом деле является объявлением шаблона функции, чей список параметров шаблона включает один придуманный параметр для каждого уникального заполнителя в порядке появления.
// короткая форма:void g1(const EqualityComparable*, Incrementable&);// длинная форма:// template<EqualityComparable T, Incrementable U> void g1(const T*, U&);// более длинная форма:// template<typename T, typename U>// void g1(const T*, U&) requires EqualityComparable<T> && Incrementable<U>; void f2(std::vector<auto*>...);// длинная форма: template<typename... T> void f2(std::vector<T*>...); void f4(auto(auto::*)(auto));// длинная форма: template<typename T, typename U, typename V> void f4(T (U::*)(V));
Все заполнители, представленные эквивалентными спецификаторами ограниченного типа, имеют один и тот же вымышленный параметр шаблона. Однако каждый неограниченный спецификатор (auto
) всегда вводит другой параметр шаблона.
void f0(Comparable a, Comparable* b);// длинная форма: template<Comparable T> void f0(T a, T* b); void f1(auto a, auto* b);// длинная форма: template<typename T, typename U> f1(T a, U* b);
Как шаблоны функций, так и шаблоны классов могут быть объявлены с помощью введения шаблона, которое имеет синтаксис имя-концепта{
список-параметров(необязательно)}
, и в этом случае ключевое слово template
не требуется: каждый параметр из списка-параметров во введении шаблона становится параметром шаблона, тип которого (тип, не тип, шаблон) определяется видом соответствующего параметра в именованном концепте.
Помимо объявления шаблона, введение в шаблон связывает предикатное ограничение (смотрите ниже), которое именует (для концептов переменных) или вызывает (для концептов функций) концепт, именованный во введении.
EqualityComparable{T}class Foo;// длинная форма: template<EqualityComparable T> class Foo;// более длинная форма: template<typename T> requires EqualityComparable<T> class Foo; template<typename T, int N, typename... Xs> concept bool Example = ...; Example{A, B, ...C}struct S1;// длинная форма// template<class A, int B, class... C> requires Example<A,B,C...> struct S1;
Для шаблонов функций введение шаблона можно комбинировать с заполнителями:
Sortable{T}void f(T, auto);// длинная форма: template<Sortable T, typename U> void f(T, U);// альтернатива с использованием только заполнителей: void f(Sortable, auto);
Этот раздел не завершён Причина: подправьте страницы шаблонов объявлений, чтобы сделать ссылку здесь |
[править]Концепты
Концепт это именованный набор требований. Определение концепта появляется в области пространства имён и имеет форму определения шаблона функции (в этом случае он называется концептом функции) или определения шаблона переменной (в этом случае он называется концептом переменной). Единственное отличие состоит в том, что ключевое слово concept появляется в последовательности-обьявления-спецификаторов:
// концепт переменной из стандартной библиотеки (Диапазоны ТС)template<class T, class U> concept bool Derived =std::is_base_of<U, T>::value; // концепт функции из стандартной библиотеки (Диапазоны ТС)template<class T> concept bool EqualityComparable(){return requires(T a, T b){{a == b}-> Boolean;{a != b}-> Boolean;};}
К концептам функций применяются следующие ограничения:
inline
иconstexpr
не разрешены, функция автоматическиinline
иconstexpr
friend
иvirtual
не допускаются- спецификация исключения не допускается, функция автоматически
noexcept(true)
. - не может быть объявлен и определён позже, не может быть объявлен повторно
- тип возвращаемого значения должен быть
bool
- вывод типа возвращаемого значения не допускается
- список параметров должен быть пустым
- тело функции должно состоять только из оператора
return
, аргумент которого должен быть выражением-ограничения (предикатным ограничением, конъюнкцией/дизъюнкцией других ограничений или выражением-requires, смотрите ниже)
К концептам переменных применяются следующие ограничения:
- Должен иметь тип
bool
- Не может быть объявлен без инициализатора
- Не может быть объявлен в области видимости класса.
constexpr
не допускается, переменная автоматическиconstexpr
- инициализатор должен быть выражением ограничения (предикатным ограничением, конъюнкцией/дизъюнкцией ограничений или выражением-requires, смотрите ниже)
Концепты не могут рекурсивно ссылаться на себя в теле функции или в инициализаторе переменной:
template<typename T> concept bool F(){return F<typename T::type>();}// ошибкаtemplate<typename T> concept bool V = V<T*>;// ошибка
Явные экземпляры, явные специализации или частичные специализации концептов не допускаются (значение исходного определения ограничения не может быть изменено)
[править]Ограничения
Ограничение это последовательность логических операций, определяющая требования к аргументам шаблона. Они могут появляться в выражениях-requires (смотрите ниже) и непосредственно как тело концептов
Есть 9 типов ограничений:
Первые три типа ограничений могут появляться непосредственно как тело концепта или как специальное предложение-requires
template<typename T> requires // предложение-requires (специальное ограничение) sizeof(T)>1&& get_value<T>()// конъюнкция двух предикатных ограниченийvoid f(T);
Когда к одному и тому же объявлению присоединено несколько ограничений, общее ограничение представляет собой конъюнкцию в следующем порядке: ограничение, введённое с помощью введения шаблона, ограничения для каждого параметра шаблона в порядке появления, предложение requires после списка параметров шаблона, ограничения для каждого параметра функции в порядке появления, завершающее предложение requires:
// объявления объявляют один и тот же шаблон ограниченной функции // с ограничением Incrementable<T> && Decrementable<T>template<Incrementable T>void f(T) requires Decrementable<T>;template<typename T> requires Incrementable<T>&& Decrementable<T>void f(T);// ok // следующие два объявления имеют разные ограничения:// первое объявление имеет Incrementable<T> && Decrementable<T>// второе объявление имеет Decrementable<T> && Incrementable<T>// Хотя они логически эквивалентны.// Второе объявление сформировано неверно, диагностика не требуется template<Incrementable T> requires Decrementable<T>void g();template<Decrementable T> requires Incrementable<T>void g();// ошибка
[править]Конъюнкция
Конъюнкция ограничений P
и Q
определяется как P && Q.
// примеры концептов из стандартной библиотеки (Диапазоны ТС)template<class T> concept bool Integral =std::is_integral<T>::value;template<class T> concept bool SignedIntegral = Integral<T>&&std::is_signed<T>::value;template<class T> concept bool UnsignedIntegral = Integral<T>&&!SignedIntegral<T>;
Конъюнкция двух ограничений выполняется, только если удовлетворяются оба ограничения. Конъюнкции оцениваются слева направо и замыкаются (если левое ограничение не выполняется, подстановка аргументов шаблона в правое ограничение не предпринимается: это предотвращает сбои из-за подстановки вне непосредственного контекста). Пользовательские перегрузки operator&&
не допускаются в конъюнкциях ограничений.
[править]Дизъюнкция
Дизъюнкция ограничений P
и Q
определяется как P || Q.
Дизъюнкция двух ограничений считается выполненной, если выполняется любое из ограничений. Дизъюнкции оцениваются слева направо и замыкаются (если левое ограничение удовлетворено, вывод аргумента шаблона в правом ограничение не предпринимается). Пользовательские перегрузки operator||
не допускаются в дизъюнкциях ограничений.
// пример ограничения из стандартной библиотеки (Диапазоны ТС)template<class T =void> requires EqualityComparable<T>()|| Same<T, void>struct equal_to;
[править]Предикатное ограничение
Предикатное ограничение это константное выражение типа bool. Оно удовлетворяется, только если оценивается как true
template<typename T> concept bool Size32 = sizeof(T)==4;
Предикатные ограничения могут указывать требования к параметрам шаблона, не являющихся типом, и к аргументам шаблона шаблона.
Предикатные ограничения должны оцениваться непосредственно как bool, преобразования не допускаются:
template<typename T>struct S {constexprexplicit operator bool()const{returntrue;}};template<typename T> requires S<T>{}// плохое предикатное ограничение: S<T>{} не является булевымvoid f(T); f(0);// ошибка: ограничение никогда не выполняется
[править]Требования
Ключевое слово 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;}
true
, если соответствующий концепт удовлетворяется, и false в противном случае: template<typename T> concept bool Addable = requires (T x){ x + x;};// выражение requires template<typename T> requires Addable<T>// предложение requires, не выражение requires T add(T a, T b){return a + b;} template<typename T> requires requires (T x){ x + x;}// специальное ограничение, обратите внимание,// что ключевое слово используется дважды T add(T a, T b){return a + b;}
Синтаксис выражения requires выглядит следующим образом:
requires ( список-параметров(необязательно)) { последовательность-требований} | |||||||||
список-параметров | — | список параметров, разделённых запятыми, как в объявлении функции, за исключением того, что аргументы по умолчанию не разрешены, а последний параметр не может быть многоточием. У этих параметров нет хранилища, связывания или времени жизни. Эти параметры находятся в области видимости до закрывающей } в последовательности-требований. Если параметры не используются, круглые скобки можно опустить |
последовательность-требований | — | разделённая пробелами последовательность требований, описанная ниже (каждое требование заканчивается точкой с запятой). Каждое требование добавляет ещё одно ограничение к конъюнкции ограничений, которое определяет это выражение requires. |
Каждое требование в последовательности-требований является одним из следующих:
- простое требование
- требования к типу
- составные требования
- вложенные требования
Требования могут относиться к параметрам шаблона, которые находятся в области видимости, и к локальным параметрам, представленным в списке-параметров. Когда параметризовано, выражение-requires, как говорят, вводит параметризованное ограничение
Подстановка аргументов шаблона в выражение-requires может привести к формированию недопустимых типов или выражений в своих требованиях. В таких случаях,
- Если ошибка подстановки возникает в выражении-requires, которое используется вне объявления шаблонной сущности, значит программа имеет неправильный формат.
- Если выражение-requires используется в объявлении шаблонной сущности, соответствующее ограничение рассматривается как "не выполнено" и сбой замены не ошибка, однако
- Если в выражении-requires для каждого возможного аргумента шаблона произойдёт ошибка замены, программа имеет неправильный формат, диагностика не требуется:
template<class T> concept bool C = requires { new int[-(int)sizeof(T)];// ошибочный для каждого T: неверно сформировано,// диагностика не требуется};
[править]Простые требования
Простое требование это произвольное выражение оператор. Требование состоит в том, чтобы выражение было действительным (это выражение ограничения). В отличие от предикатных ограничений, оценка не выполняется, проверяется только правильность языка.
template<typename T> concept bool Addable = requires (T a, T b){ a + b;// "выражение a+b является допустимым выражением, которое будет скомпилировано"}; // пример ограничения из стандартной библиотеки (Диапазоны ТС)template<class T, class U = T> concept bool Swappable = requires(T&& t, U&& u){ swap(std::forward<T>(t), std::forward<U>(u)); swap(std::forward<U>(u), std::forward<T>(t));};
[править]Требования к типу
Требование к типу это ключевое слово typename за которым следует имя типа, необязательно уточнённое. Требование состоит в том, что именованный тип существует (ограничение типа): это можно использовать для проверки того, что существует определённый именованный вложенный тип, или что специализация шаблона класса именует тип, или что псевдоним шаблона именует тип.
template<typename T>using Ref = T&;template<typename T> concept bool C = requires {typename T::inner;// требуется имя вложенного элементаtypename S<T>;// требуется специализация шаблона классаtypename Ref<T>;// требуется подстановка псевдонима шаблона}; //Пример концепта из стандартной библиотеки (Диапазоны ТС)template<class T, class U>using CommonType =std::common_type_t<T, U>;template<class T, class U> concept bool Common = requires (T t, U u){typename CommonType<T, U>;// CommonType<T, U> действителен и именует тип{ CommonType<T, U>{std::forward<T>(t)}};{ CommonType<T, U>{std::forward<U>(u)}};};
[править]Составные Требования
Составное требование имеет форму
{ выражение} noexcept (необязательно)конечный-возвращаемый-тип(необязательно); | |||||||||
и определяет конъюнкцию следующих ограничений:
noexcept
, выражение также должно быть noexcept (ограничение исключения)template<typename T> concept bool C2 = requires(T x){{*x}->typename T::inner;// выражение *x должно быть действительным// И тип T::inner должен быть действительным// И результат *x должен быть преобразован в T::inner}; // Пример концепта из стандартной библиотеки (ТС Диапазонов)template<class T, class U> concept bool Same =std::is_same<T,U>::value;template<class B> concept bool Boolean = requires(B b1, B b2){{bool(b1)};// ограничение прямой инициализации должно использовать выражение{!b1 }->bool;// составное ограничение requires Same<decltype(b1 && b2), bool>;// вложенное ограничение, смотрите ниже requires Same<decltype(b1 || b2), bool>;};
[править]Вложенные требования
Вложенное требование это еще одно предложение-requires, заканчивающееся точкой с запятой. Оно используется для введения предикатных ограничений (смотрите выше), выраженных в терминах других именованных концептов, применяемых к локальным параметрам (вне предложения requires, предикатные ограничения не могут использовать параметры и размещать выражения непосредственно в предложении requires, делая его ограничением выражения, что означает, что оно не оценивается)
// пример ограничения из ТС Диапазоновtemplate<class T> concept bool Semiregular = DefaultConstructible<T>&& CopyConstructible<T>&& Destructible<T>&& CopyAssignable<T>&& requires(T a, size_t n){ requires Same<T*, decltype(&a)>;// вложенное: "Same<...> оценивается как true"{ a.~T()}noexcept;// составное: "a.~T()" допустимое выражение,// которое не бросает исключение requires Same<T*, decltype(new T)>;// вложенное: "Same<...> оценивается как true" requires Same<T*, decltype(new T[n])>;// вложенное{ delete new T };// составное{ delete new T[n]};// составное};
[править]Разрешение концепта
Как и любой другой шаблон функции, концепт функции (но не концепт переменной) может быть перегружен: может быть предоставлено несколько определений концепта, которые все используют одно и то же имя-концепта.
Разрешение концепта выполняется, когда имя-концепта (которое может быть квалифицировано) появляется в
template<typename T> concept bool C(){returntrue;}// #1template<typename T, typename U> concept bool C(){returntrue;}// #2void f(C);// набор концептов, на который ссылается C, включенный как #1 или #2;// разрешение концепта (смотрите ниже) выбирает #1.
Чтобы выполнить разрешение концепта, параметры шаблона каждого концепта, который соответствует имени (и квалификации, если таковая имеется), сопоставляются с последовательностью аргументов концепта, которые являются аргументами шаблона и подстановочными знаками. Подстановочный знак может соответствовать параметру шаблона любого типа (тип, не тип, шаблон). Набор аргументов строится по-разному, в зависимости от контекста
template<typename T> concept bool C1(){returntrue;}// #1template<typename T, typename U> concept bool C1(){returntrue;}// #2void f1(const C1*);// <подстановочный знак> соответствует <T>, выбирает #1
template<typename T> concept bool C1(){returntrue;}// #1template<typename T, typename U> concept bool C1(){returntrue;}// #2void f2(C1<char>);// <подстановочный знак, char> соответствует <T, U>, выбирает #2
template<typename... Ts> concept bool C3 =true; C3{T}void q2();// OK: <T> соответствует <...Ts> C3{...Ts}void q1();// OK: <...Ts> соответствует <...Ts>
template<typename T> concept bool C(){returntrue;}// #1template<typename T, typename U> concept bool C(){returntrue;}// #2 template<typename T>void f(T) requires C<T>();// соответствует #1
Разрешение концепта выполняется путём сопоставления каждого аргумента с соответствующим параметром каждого видимого концепта. Аргументы шаблона по умолчанию (если они используются) создаются для каждого параметра, который не соответствует аргументу, а затем добавляются в список аргументов. Параметр шаблона соответствует аргументу, только если он имеет тот же тип (тип, не тип, шаблон), если только аргумент не является подстановочным знаком. Пакет параметров соответствует нулю или большему количеству аргументов, пока все аргументы соответствуют образцу таким же образом (если только они не являются подстановочными знаками).
Если какой-либо аргумент не соответствует соответствующему параметру или если аргументов больше, чем параметров, и последний параметр не является пакетом, концепт не является реализуемым. Если существует ноль или более одного нереализуемого концепта, программа неверно сформирована.
template<typename T> concept bool C2(){returntrue;}template<int T> concept bool C2(){returntrue;} template<C2<0> T>struct S1;// ошибка: <подстановочный знак, 0> не соответствует // ни <typename T> ни <int T>template<C2 T>struct S2;// совпадение #1 и #2: ошибка
Этот раздел не завершён Причина: нужен пример с осмысленными концептами, а не с этими 'return true' заполнителями |
[править]Частичное упорядочивание ограничений
Перед любым дальнейшим анализом ограничения нормализуются путём подстановки тела каждого имени концепта и каждого выражения requires до тех пор, пока не останется последовательность конъюнкций и дизъюнкций атомарных ограничений, которые являются предикатными ограничениями, ограничениями выражений, ограничениями типов, ограничениями неявных преобразований, ограничениями вывода аргументов и ограничениями исключений.
Говорят, что концепт P
включает концепт Q
, если можно доказать, что P
заключает в себеQ
без анализа типов и выражений на эквивалентность (поэтому N >= 0
не включает N > 0
)
В частности, сначала P
преобразуется в дизъюнктивную нормальную форму, а Q
преобразуется в конъюнктивную нормальную форму, и они сравниваются следующим образом:
- каждое атомарное ограничение
A
включает эквивалентное атомарное ограничениеA
- каждое атомарное ограничение
A
включает дизъюнкциюA||B
и не включает конъюнкциюA&&B
- каждая конъюнкция
A&&B
включаетA
, но дизъюнкцияA||B
не включаетA
Отношение подчинения определяет частичный порядок ограничений, который используется для определения:
- лучшего жизнеспособного кандидата для нешаблонной функции в разрешении перегрузки
- адреса нешаблонной функции в наборе перегрузки
- наилучшего соответствия аргументу шаблона шаблона
- частичного упорядочивания специализаций шаблонов классов
- частичного упорядочивания шаблонов функций
Этот раздел не завершён Причина: обратные ссылки с вышеупомянутого сюда |
Если объявления D1
и D2
ограничены, а нормализованные ограничения D1 подпадают под нормализованные ограничения D2 (или если D1 ограничен, а D2 не ограничен), то говорят, что D1 по крайней мере ограничен так же, как D2. Если D1 ограничен как минимум так же, как D2, а D2 не ограничен как минимум так же, как D1, то D1 более ограничен, чем D2.
template<typename T> concept bool Decrementable = requires(T t){--t;};template<typename T> concept bool RevIterator = Decrementable<T>&& requires(T t){*t;}; // RevIterator включает Decrementable, но не наоборот// RevIterator более ограничен, чем Decrementable void f(Decrementable);// #1void f(RevIterator);// #2 f(0);// int соответствует только Decrementable, выбирает #1 f((int*)0);// int* соответствует обоим ограничениям, выбирает #2 как более ограниченный void g(auto);// #3 (неограниченный)void g(Decrementable);// #4 g(true);// bool не соответствует Decrementable, выбирает #3 g(0);// int соответствует Decrementable, выбирает #4, потому что он более ограничен
[править]Ключевые слова
[править]Поддержка компиляторов
GCC >= 6.1 поддерживает эту техническую спецификацию (обязательный параметр -fconcepts)