Spec-Zone.ru › C++

Ограничения и концепты (начиная с C++20)

Шаблоны классов, шаблоны функций (включая обобщённые лямбда-выражения) и другие шаблонные функции (обычно являющиеся членами шаблонов классов) могут быть связаны с ограничением , которое задаёт требования к аргументам шаблона и может использоваться для выбора наиболее подходящих перегрузок функций и специализаций шаблонов.

Именованные наборы таких требований называются концептами . Каждый концепт является предикатом, вычисляемым во время компиляции, и становится частью интерфейса шаблона, в котором он используется в качестве ограничения:

#include <cstddef>
#include <concepts>
#include <functional>
#include <string>

// Declaration of the concept “Hashable”, which is satisfied by any type “T”
// such that for values “a” of type “T”, the expression std::hash<T>{}(a)
// compiles and its result is convertible to std::size_t
template<typename T>
concept Hashable = requires(T a)
{
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

struct meow {};

// Constrained C++20 function template:
template<Hashable T>
void f(T) {}
//
// Alternative ways to apply the same constraint:
// 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 satisfies Hashable
    // f(meow{}); // Error: meow does not satisfy Hashable
}

Нарушения ограничений обнаруживаются во время компиляции, на раннем этапе инстанцирования шаблона, что позволяет получать понятные сообщения об ошибках:

std::list<int> l = {3, -1, 10};
std::sort(l.begin(), l.end()); 
// Typical compiler diagnostic without concepts:
// invalid operands to binary expression ('std::_List_iterator<int>' and
// 'std::_List_iterator<int>')
//                           std::__lg(__last - __first) * 2);
//                                     ~~~~~~ ^ ~~~~~~~
// ... 50 lines of output ...
//
// Typical compiler diagnostic with concepts:
// 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, «Возможность задавать осмысленную семантику — определяющая характеристика настоящего концепта, в отличие от синтаксического ограничения».

Концепты

Концепт — это именованный набор требований. Определение концепта должно находиться на уровне пространства имён.

Определение концепта имеет следующий вид

template < template-parameter-list >

concept concept-name attr (необязательно) = constraint-expression;

attr - последовательность из любого количества атрибутов
// concept
template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;

Концепты не могут рекурсивно ссылаться на себя и не могут иметь ограничений:

template<typename T>
concept V = V<T*>; // error: recursive concept

template<class T>
concept C1 = true;
template<C1 T>
concept Error1 = true; // Error: C1 T attempts to constrain a concept definition
template<class T> requires C1<T>
concept Error2 = true; // Error: the requires clause attempts to constrain a concept

Явные инстанцирования, явные специализации и частичные специализации концептов не допускаются (смысл исходного определения ограничения нельзя изменить).

Концепты могут быть указаны в id-выражении. Значение id-выражения равно true, если выражение ограничения удовлетворено, и false в противном случае.

Концепты также могут быть указаны в ограничении типа в составе

  • объявления параметра шаблона типа,
  • спецификатора типа-заполнителя,
  • составного требования.

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

template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;

template<Derived<Base> T>
void f(T); // T is constrained by Derived<T, Base>

Ограничения

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

Существует три(до C++26)четыре(начиная с C++26) типа ограничений:

1) конъюнкции
2) дизъюнкции
3) атомарные ограничения
4) ограничения, раскрытые свёрткой (начиная с C++26)

Ограничение, связанное с объявлением, определяется путём нормализации логического выражения И, операнды которого расположены в следующем порядке:

  1. выражение ограничения, введённое для каждого ограниченного параметра шаблона типа или параметра шаблона не-типа, объявленного с ограниченным типом-заполнителем, в порядке их появления;
  2. выражение ограничения в предложении requires после списка параметров шаблона;
  3. выражение ограничения, введённое для каждого параметра с ограниченным типом-заполнителем в объявлении сокращённого шаблона функции;
  4. выражение ограничения в завершающем предложении requires.

Этот порядок определяет последовательность инстанцирования ограничений при проверке их удовлетворения.

Повторные объявления

Ограниченное объявление может быть повторно объявлено только с использованием той же синтаксической формы. Диагностика не требуется:

// These first two declarations of f are fine
template<Incrementable T>
void f(T) requires Decrementable<T>;

template<Incrementable T>
void f(T) requires Decrementable<T>; // OK, redeclaration

// Inclusion of this third, logically-equivalent-but-syntactically-different
// declaration of f is ill-formed, no diagnostic required
template<typename T>
    requires Incrementable<T> && Decrementable<T>
void f(T);

// The following two declarations have different constraints:
// the first declaration has Incrementable<T> && Decrementable<T>
// the second declaration has Decrementable<T> && Incrementable<T>
// Even though they are logically equivalent.

template<Incrementable T> 
void g(T) requires Decrementable<T>;

template<Decrementable T> 
void g(T) requires Incrementable<T>; // ill-formed, no diagnostic required

Конъюнкции

Конъюнкция двух ограничений формируется с помощью оператора && в выражении ограничения:

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, calls #2. When checking the constraints of #1,
            // 'sizeof(char) > 1' is not satisfied, so get_value<T>() is not checked
}

Дизъюнкции

Дизъюнкция двух ограничений формируется с помощью оператора || в выражении ограничения.

Дизъюнкция двух ограничений удовлетворяется, если удовлетворяется хотя бы одно из них. Дизъюнкции вычисляются слева направо с коротким замыканием (если левое ограничение удовлетворяется, подстановка аргументов шаблона в правое ограничение не выполняется).

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); // error: S<int>{} does not have type bool when checking #1,
          // even though #2 is a better match
}

Два атомарных ограничения считаются идентичными, если они образованы одним и тем же выражением на уровне исходного кода и их отображения параметров эквивалентны.

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); // error, ambiguous:
           // the is_meowable<T> in Meowable and BadMeowableCat forms distinct atomic
           // constraints that are not identical (and so do not subsume each other)
    
    f2(0); // OK, calls #4, more constrained than #3
           // GoodMeowableCat got its is_meowable<T> from Meowable
}

Ограничения, раскрытые свёрткой

Ограничение, раскрытое свёрткой, формируется из ограничения C и оператора свёртки (либо &&, либо ||). Ограничение, раскрытое свёрткой, является раскрытием пакета.

Пусть N — количество элементов в параметрах раскрытия пакета:

  • Если раскрытие пакета недопустимо (например, раскрываются пакеты разного размера), ограничение, раскрытое свёрткой, не удовлетворяется.
  • Если N равно 0, ограничение, раскрытое свёрткой, удовлетворяется, если оператор свёртки — &&, и не удовлетворяется, если оператор свёртки — ||.
  • Для ограничения, раскрытого свёрткой, с положительным N, для каждого i в [1, N] каждый параметр раскрытия пакета заменяется соответствующим i-м элементом в порядке возрастания:
    • Для ограничений, раскрытых свёрткой с оператором свёртки &&, если замена j-го элемента нарушает C, ограничение, раскрытое свёрткой, не удовлетворяется. В этом случае подстановка не выполняется для любого i, большего j. В противном случае ограничение, раскрытое свёрткой, удовлетворяется.
    • Для ограничений, раскрытых свёрткой с оператором свёртки ||, если замена j-го элемента удовлетворяет C, ограничение, раскрытое свёрткой, удовлетворяется. В этом случае подстановка не выполняется для любого i, большего j. В противном случае ограничение, раскрытое свёрткой, не удовлетворяется.
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>;

// in C++23, these two overloads of g() have distinct atomic constraints 
// that are not identical and so do not subsume each other: calls to g() are ambiguous
// in C++26, the folds are expanded and constraint on overload #2 (both move and copy
// required), subsumes constraint on overload #1 (just the move is required)
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: normalized to the disjunction of 
                   // - T::value (with mapping T -> U*) and
                   // - true (with an empty mapping).
                   // No invalid type in mapping even though
                   // T::value is ill-formed for all pointer types

template<typename V>
concept C = B<V&>; // Normalizes to the disjunction of
                   // - T::value (with mapping T-> V&*) and
                   // - true (with an empty mapping).
                   // Invalid type V&* formed in mapping => ill-formed NDR
  • Нормальная форма выражений (E && ...) и (... && E) — это ограничение, раскрытое свёрткой, где C является нормальной формой E, а оператором свёртки является &&.
  • Нормальная форма выражений (E || ...) и (... || E) — это ограничение, раскрытое свёрткой, где C является нормальной формой E, а оператором свёртки является ||.
  • Нормальные формы выражений (E1 && ... && E2) и (E1 || ... || E2) являются нормальными формами
    • (E1 && ...) && E2 и (E1 || ...) || E2 соответственно, если E1 содержит нераскрытый пакет, или
    • E1 && (... && E2) и E1 || (... || E2) соответственно в противном случае.
(начиная с C++26)
  • Нормальная форма любого другого выражения E — это атомарное ограничение, выражением которого является E, а отображением параметров — тождественное отображение. Это относится ко всем выражениям свёртки, включая свёртки с операторами && или ||.

Пользовательские перегрузки && или || не влияют на нормализацию ограничений.

Предложения requires

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

template<typename T>
void f(T&&) requires Eq<T>; // can appear as the last element of a function declarator

template<typename T> requires Addable<T> // or right after a template parameter list
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>(); // error, is_purrable<T>() is not a primary expression

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 тогда и только тогда, когда они идентичны согласно правилам, описанным выше.
  • Ограничение, раскрытое свёрткой, A поглощает другое ограничение, раскрытое свёрткой, B, если у них одинаковый оператор свёртки, ограничение C из A поглощает ограничение B, и оба C содержат эквивалентный нераскрытый пакет.
(начиная с 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 subsumes Decrementable, but not the other way around

template<Decrementable T>
void f(T); // #1

template<RevIterator T>
void f(T); // #2, more constrained than #1

f(0);       // int only satisfies Decrementable, selects #1
f((int*)0); // int* satisfies both constraints, selects #2 as more constrained

template<class T>
void g(T); // #3 (unconstrained)

template<Decrementable T>
void g(T); // #4

g(true); // bool does not satisfy Decrementable, selects #3
g(0);    // int satisfies Decrementable, selects #4 because it is more constrained

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); // ambiguous

Примечания

Макрос проверки поддержки возможностей Значение Стандарт Возможность
__cpp_concepts 201907L (C++20) Ограничения
202002L (C++20) Условно тривиальные специальные функции-члены

Ключевые слова

concept, requires, typename

Отчёты об ошибках

Следующие отчёты об ошибках, изменяющие поведение, были применены задним числом к ранее опубликованным стандартам C++.

Отчёт об ошибке Применён к Опубликованное поведение Правильное поведение
CWG 2428 C++20 к концептам нельзя было применять атрибуты разрешено

См. также

  • Техническая спецификация концептов
  • Именованные требования
Выражение requires(C++20) возвращает выражение prvalue типа bool, описывающее ограничения

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/concepts

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API