Spec-Zone.ru › C++

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

Эта страница описывает ключевую языковую функцию, принятую для C++20. Для требований к именованным типам, используемым в спецификации стандартной библиотеки, см. требования к именованным типам. Для версии этой функции от Concepts TS см. здесь.

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

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

#include <cstddef>
#include <concepts>
#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 /*parameterName*/) {}
 
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

Цель концепций — моделировать семантические категории (Число, Диапазон, Регулярная функция) вместо синтаксических ограничений (Есть оператор +, Массив). Согласно основному руководству по C++ T.20, «возможность указывать осмысленную семантику является определяющим признаком истинной концепции по сравнению с синтаксическим ограничением».

Концепции

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

Определение концепции имеет вид

template < список параметров шаблона >

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

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 или непосредственно как тела концепций.

Существует три типа ограничений:

1) конъюнкции
2) дизъюнкции
3) атомарные ограничения

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

  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, должно быть правым константным выражением типа 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
}

Нормализация ограничений

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

  • Нормальная форма выражения (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 и отображение параметров которого — тождественное отображение. Это включает все выражения свертки, даже те, которые сворачиваются по операторам && или ||.

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

Операторные части 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 тогда и только тогда, когда они идентичны, используя правила, описанные выше.

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

  • лучшего жизнеспособного кандидата для нешаблонной функции в разрешении перегрузки
  • адреса нешаблонной функции в наборе перегрузок адреса нешаблонной функции
  • лучшего соответствия для шаблонного шаблонного аргумента
  • частичного порядка специализаций шаблонных классов
  • частичного порядка шаблонных функций

Если объявления 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 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

Примечания

Макрос проверки функций Значение Std Функция
__cpp_concepts 201907L (C++20) Ограничения
202002L (C++20) Условно тривиальные специальные члены функций

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

concept, requires

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

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

DR Применено к Поведение, как опубликовано Корректное поведение
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/w/cpp/language/constraints

Spec-Zone.ru

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