Namespaces
Variants

Constraints and concepts (since C++20)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

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

#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 < список-параметров-шаблона >

concept имя-концепта attr  (необязательно) = выражение-ограничения ;

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) типа ограничений:

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

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

  1. ограничивающее выражение, введённое для каждого ограниченного параметра шаблона типа или постоянного параметра шаблона, объявленного с ограниченным типом-заполнителем , в порядке их появления;
  2. ограничивающее выражение в requires clause после списка параметров шаблона;
  3. ограничивающее выражение, введённое для каждого параметра с ограниченным типом-заполнителем в объявлении сокращённого шаблона функции ;
  4. ограничивающее выражение в завершающей 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
}

Свернутые развернутые ограничения

Свернутое развернутое ограничение формируется из ограничения 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>;
// в 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
  • Нормальная форма выражений ( 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>; // может появляться как последний элемент декларатора функции
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 тогда и только тогда, когда они идентичны согласно правилам, описанным выше .
  • Свернутое развернутое ограничение 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 включает 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) Условно тривиальные специальные функции-члены

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

concept , requires , typename

Отчеты о дефектах

Следующие отчеты об изменениях поведения, влияющие на дефекты, были применены задним числом к ранее опубликованным стандартам 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 , описывающее ограничения