Constraints and concepts
На этой странице описывается экспериментальная возможность основного языка. Для именованных требований к типам, используемых в спецификации стандартной библиотеки, см. named requirements
Шаблоны классов , шаблоны функций и нешаблонные функции (обычно члены шаблонов классов) могут быть ассоциированы с ограничением , которое определяет требования к аргументам шаблона и может использоваться для выбора наиболее подходящих перегрузок функций и специализаций шаблонов.
Ограничения также могут использоваться для ограничения автоматического вывода типов в объявлениях переменных и возвращаемых типах функций только теми типами, которые удовлетворяют указанным требованиям.
Названные наборы таких требований называются concepts . Каждый концепт является предикатом, вычисляемым во время компиляции, и становится частью интерфейса шаблона, где он используется как ограничение:
#include <string> #include <locale> using namespace std::literals; // Объявление концепта "EqualityComparable", который удовлетворяется // любым типом T, для которого для значений a и b типа T // выражение a==b компилируется и его результат преобразуется в bool template<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++ core guideline T.20 , "Возможность задать осмысленную семантику является определяющей характеристикой истинного концепта в отличие от синтаксического ограничения."
Если тестирование возможностей поддерживается, возможности, описанные здесь, обозначаются макроконстантой __cpp_concepts со значением равным или большим 201507 .
Содержание |
Заполнители
Неограниченный заполнитель
auto
и
ограниченные заполнители
, имеющие форму
concept-name
<
template-argument-list
(optional)
>
, являются заполнителями для типа, который должен быть выведен.
Заполнители могут появляться в объявлениях переменных (в этом случае они выводятся из инициализатора) или в возвращаемых типах функций (в этом случае они выводятся из return-выражений)
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>); // корректно
Сокращённые шаблоны
Если один или несколько заполнителей появляются в списке параметров функции, объявление функции фактически является объявлением шаблона функции, чей список параметров шаблона включает один изобретенный параметр для каждого уникального заполнителя, в порядке их появления
// краткая форма 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);
Как шаблоны функций, так и шаблоны классов могут быть объявлены с использованием
введения шаблона
, которое имеет синтаксис
concept-name
{
parameter-list
(optional)
}
, в этом случае ключевое слово
template
не требуется: каждый параметр из
parameter-list
введения шаблона становится параметром шаблона, тип которого (тип, не-тип, шаблон) определяется типом соответствующего параметра в указанной концепции.
Помимо объявления шаблона, введение шаблона связывает предикатное ограничение (см. ниже), которое именует (для концептов переменных) или вызывает (для концептов функций) концепт, указанный введением.
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 появляется в последовательности-спецификаторов-объявления :
// концепция переменной из стандартной библиотеки (Ranges TS) template <class T, class U> concept bool Derived = std::is_base_of<U, T>::value; // концепция функции из стандартной библиотеки (Ranges TS) 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, чей аргумент должен быть constraint-expression (предикатное ограничение, конъюнкция/дизъюнкция других ограничений или 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-выражение (ad-hoc ограничение) 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); // корректно // следующие два объявления имеют разные ограничения: // первое объявление имеет 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
.
// пример концепций из стандартной библиотеки (Ranges TS) 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||
не разрешены в дизъюнкциях ограничений.
// пример ограничения из стандартной библиотеки (Ranges TS) 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 { constexpr explicit operator bool() const { return true; } }; template<typename T> requires S<T>{} // некорректное ограничение-предикат: S<T>{} не является bool 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-expression template<typename T> requires Addable<T> // requires-clause, not requires-expression T add(T a, T b) { return a + b; } template<typename T> requires requires (T x) { x + x; } // ad-hoc constraint, note keyword used twice T add(T a, T b) { return a + b; }
Синтаксис requires-expression выглядит следующим образом:
requires
(
список-параметров
(опционально)
)
{
последовательность-требований
}
|
|||||||||
| parameter-list | - |
список параметров, разделенных запятыми, как в объявлении функции, за исключением того, что аргументы по умолчанию не допускаются и последний параметр не может быть многоточием. Эти параметры не имеют хранилища, связывания или времени жизни. Эти параметры находятся в области видимости до закрывающей скобки
}
последовательности
requirement-seq
. Если параметры не используются, круглые скобки также могут быть опущены.
|
| requirement-seq | - | последовательность требований , разделенных пробелами, описана ниже (каждое требование заканчивается точкой с запятой). Каждое требование добавляет еще одно ограничение к конъюнкции ограничений, которую определяет это requires-выражение. |
Каждое требование в requirements-seq является одним из следующих:
- простое требование
- требования к типу
- составное требование
- вложенное требование
Требования могут ссылаться на параметры шаблона, находящиеся в области видимости, и на локальные параметры, введенные в parameter-list . При параметризации считается, что 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 является корректным выражением, которое скомпилируется" }; // пример ограничения из стандартной библиотеки (ranges TS) 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>; // требуется подстановка шаблона псевдонима }; // Пример концепции из стандартной библиотеки (Ranges TS) 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)} }; };
Составные требования
Составное требование имеет вид
{
expression
}
noexcept
(необязательно)
trailing-return-type
(необязательно)
;
|
|||||||||
и задаёт конъюнкцию следующих ограничений:
noexcept
, выражение также должно быть noexcept (
ограничение исключений
)
template<typename T> concept bool C2 = requires(T x) { {*x} -> typename T::inner; // выражение *x должно быть валидным // И тип T::inner должен быть валидным // И результат *x должен быть конвертируемым в T::inner }; // Пример концепции из стандартной библиотеки (Ranges TS) 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-директиве делает его выраженческим ограничением, что означает, что оно не вычисляется)
// пример ограничения из Ranges TS 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] }; // составное };
Разрешение концепций
Как и любой другой шаблон функции, функциональное понятие (но не понятие-переменная) может быть перегружено: можно предоставить несколько определений понятия, которые все используют одно и то же concept-name .
Разрешение концепции выполняется, когда concept-name (который может быть квалифицированным) появляется в
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 void f(C); // набор концептов, на которые ссылается C, включает как #1, так и #2; // разрешение концептов (см. ниже) выбирает #1.
Для выполнения разрешения концепций, template parameters каждого концепта, соответствующего имени (и квалификации, если она есть), сопоставляются с последовательностью concept arguments , которые являются аргументами шаблона и wildcards . Wildcard может соответствовать параметру шаблона любого вида (тип, не-тип, шаблон). Набор аргументов конструируется по-разному в зависимости от контекста.
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f1(const C1*); // <wildcard> matches <T>, selects #1
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f2(C1<char>); // <wildcard, char> matches <T, U>, selects #2
template<typename... Ts> concept bool C3 = true; C3{T} void q2(); // OK: <T> matches <...Ts> C3{...Ts} void q1(); // OK: <...Ts> matches <...Ts>
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 template <typename T> void f(T) requires C<T>(); // matches #1
Разрешение концепций выполняется путем сопоставления каждого аргумента с соответствующим параметром каждого видимого концепта. Аргументы шаблона по умолчанию (если используются) инстанцируются для каждого параметра, который не соответствует аргументу, и затем добавляются к списку аргументов. Параметр шаблона соответствует аргументу только если он имеет тот же вид (тип, не-тип, шаблон), за исключением случая когда аргумент является wildcard. Параметр-пакет соответствует нулю или более аргументам при условии, что все аргументы соответствуют шаблону по виду (за исключением случаев когда они являются wildcard).
Если какой-либо аргумент не соответствует своему параметру или если аргументов больше, чем параметров, и последний параметр не является паковкой, концепция нежизнеспособна. Если жизнеспособных концепций ноль или больше одной, программа некорректна.
template<typename T> concept bool C2() { return true; } template<int T> concept bool C2() { return true; } template<C2<0> T> struct S1; // ошибка: <wildcard, 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); // #1 void 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 )