Пространства имён
Варианты
Действия

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

Материал из cppreference.com
< cpp‎ | language
 
 
Язык С++
Общие темы
Управление потоком
Операторы условного выполнения
if
Операторы итерации (циклы)
Операторы перехода
Функции
Объявление функции
Выражение лямбда-функции
Спецификатор inline
Спецификации динамических исключений (до C++17*)
Спецификатор noexcept (C++11)
Исключения
Пространства имён
Типы
Спецификаторы
decltype (C++11)
auto (C++11)
alignas (C++11)
Спецификаторы длительности хранения
Инициализация
Выражения
Альтернативные представления
Литералы
Логические - Целочисленные - С плавающей запятой
Символьные - Строковые - nullptr (C++11)
Определяемые пользователем (C++11)
Утилиты
Атрибуты (C++11)
Types
Объявление typedef
Объявление псевдонима типа (C++11)
Casts
Неявные преобразования - Явные преобразования
static_cast - dynamic_cast
const_cast - reinterpret_cast
Выделение памяти
Классы
Свойства функции класса
explicit (C++11)
static
Специальные функции-элементы
Шаблоны
Разное
 
 
На этой странице описывается основная функциональность языка, адаптированная для C++20. Требования к именованным типам, используемые в спецификации стандартной библиотеки, смотрите в именованных требованиях. Версию этой функциональности для Концепты ТС смотрите здесь.

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

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

#include <string>
#include <cstddef>
#include <concepts>
 
// Объявление концепта "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 /*parameterName*/) {}
 
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()); 
// Типичная диагностика компилятора без концептов:
// недопустимые операнды для бинарного выражения ('std::_List_iterator<int>' и
// 'std::_List_iterator<int>')
//                           std::__lg(__last - __first) * 2);
//                                     ~~~~~~ ^ ~~~~~~~
// ... 50 строк вывода ...
//
// Типичная диагностика компилятора с концептами:
// ошибка: невозможно вызвать std::sort с помощью std::_List_iterator<int>
// примечание: концепт RandomAccessIterator<std::_List_iterator<int>> не был удовлетворён

Назначение концептов моделировать семантические категории (Number, Range, RegularFunction), а не синтаксические ограничения (HasPlus, Array). Согласно Основному Руководству ISO C++ T.20, "Возможность указывать осмысленную семантику является определяющей характеристикой истинного концепта, а не синтаксическим ограничением."

Содержание

[править] Концепты

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

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

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

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

атрибуты последовательность любого количества атрибутов
// концепт
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 пытается ограничить концепт

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

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

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

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

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

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

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

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

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

Этот порядок определяет порядок, в котором реализуются ограничения при проверке на соответствие.

[править] Повторное объявление

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

// Эти первые два объявления 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-в-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
}

[править] Нормализация ограничений

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

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

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

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

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

Если объявления 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 включает в себя 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

[править] Отчёты о дефектах

Следующие изменения поведения были применены с обратной силой к ранее опубликованным стандартам C++:

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

[править] Смотрите также

Выражение requires(C++20) даёт выражение prvalue типа bool, описывающее ограничения[править]