Выражение requires призвано конкретизировать и детализировать ограничения. Оно имеет следующие формы:
requires { требования } requires (параметры) { требования }
После слова requires
может идти необязательный список параметров в круглых скобках, который во многом аналогичен списку параметров функции.
За списком параметров в фигурных скобках указываются требования, которые могут использовать параметры. Каждое требование заканчивается точкой с запятой.
Требования могут быть простыми и составными. При этом параметры выражения require никогда не привязываются к
фактическим аргументам, а выражения в фигурных скобках никогда не выполняются. Все, что компилятор делает с этими выражениями, — это проверяет,
образуют ли они допустимый код C++.
Простое требование представляет произвольное выражение C++. И если это выражение для указанных типов допустимо, то тип удовлетворяет этому требованию.
Для выражения requires может быть задано множество требований, и тип должен удовлетворять всем этим требования. Например:
#include <iostream> template <typename T> concept operation = requires (T item) { item + item; item - item; item * item; }; class Counter{}; int main() { std::cout << std::boolalpha << operation<int> << std::endl; // true std::cout << std::boolalpha << operation<char> << std::endl; // true std::cout << std::boolalpha << operation<std::string> << std::endl; // false std::cout << std::boolalpha << operation<Counter> << std::endl; // false }
Здесь определен концепт operation, ограничения которого определяются с помощью выражения requires
:
requires (T item) { item + item; item - item; item * item; };
Выражение requires
определяет один параметр типа T, который мы проверяем на соответствие требованиям. В данном случае определено три требования:
item + item
, item - item
и item * item
. То есть мы берем некий тип T и проверяем, будут ои для объекта этого типа допустимы подобные выражения.
И чтобы эти выражения были допустимы, для типа T должны быть определены операции сложения, вычитания и умножения. Причем тип T должен соответствовать всем
этим требованиям.
В функции main проверяем различные типы на соответствие концепту operation. Например:
operation<int>
Для типа int
определены все заявленые операции - и сложение, и вычитание, и умножение. Соответственно данное выражение возвратит true
.
То же самое касается типа char
в случае с выражением operation<char>
А вот типа std::string
поддерживает только операцию сложения (объединение строк), поэтому следующее выражение возратит false:
operation<std::string> // false
То же самое касается нашего пустого класса Counter, для которого вообще не определено никаких операций:
operation<Counter> // false
В качестве выражений в блоке requires
можно использовать не только арифметические или другие операции. Это могут быть любые выражения:
вызовы функций, конструкторов, преобразования типов, доступ к членам класса и т. д. Однако нельзя определять локальные переменные внутри фигурных скобок.
Все переменные, которые надо использовать в выражениях, должны быть либо глобальными переменными, либо переменными, представленными в списке параметров. Например, определим
дополнительный параметр типа int:
#include <iostream> #include <vector> template <typename T> concept is_collection = requires (T collection, int n) { collection[n]; }; int main() { std::cout << std::boolalpha << is_collection<int> << std::endl; // false std::cout << std::boolalpha << is_collection<char[]> << std::endl; // true std::cout << std::boolalpha << is_collection<std::string> << std::endl; // truee std::cout << std::boolalpha << is_collection<std::vector<int>> << std::endl; // true }
Здесь определен концепт is_collection
. Он проверяет, является ли тип T коллекцией, в которой можно обратиться к элементам по индексам.
Выражение requires
теперь принимает два параметра. Второй параметр представляет тип int.
requires (T collection, int n) { collection[n]; };
Здесь используется одно требование - collection[n]
. То есть тип T должен поддерживать обращение к элементам по целочисленному индексу.
Тип int
не является коллекцией, соответственно для этого типа будет возвращаться false:
is_collection<int> // false
А вот строки, массивы, векторы поддерживают обращение по индексу, поэтому для них будет возвращаться true:
is_collection<char[]> // true is_collection<std::string> // true is_collection<std::vector<int>> // true
Применим выражение requires
для создания концепта для ограничения шаблона:
#include <iostream> #include <concepts> template <typename T> concept sum_types = requires (T x) { x + x;} ; // T должен поддерживать операцию + template <sum_types T> T sum(T a, T b){ return a + b;} int main() { std::cout << sum(10, 3) << std::endl; // 13 std::cout << sum(10.6, 3.2) << std::endl; // 13.8 std::cout << sum(std::string("Hello "), std::string("world")) << std::endl; // Hello world }
В данном случае ограничение
requires (T x) { x + x;} ;
указывает, что T может представлять любой тип, который поддерживает операцию сложения. Это могут быть и числа, и строки std::string.
Также можно использовать выражение requires
напрямую после оператора requires:
template <typename T> requires requires (T x) { x + x; } T sum(T a, T b){ return a + b;}
В данном случае первое слово requires
представляет оператор, который устанавливает ограничение для шаблона. А второе слово requires
представляет выражение, которое определяет требования.
Составное требование похоже на простое требование. Но при этом оно может также запрещать выражению генерировать исключение и/или ограничивать тип, который оно оценивает. Составное требование может принимать следующие формы
{ expr }; // expr - любое допустимое выражение { expr } noexcept; // expr валидно, если никогда не генерируется исключение { expr } -> ограничение_типа; // expr валидно, если тип удовлетворяет ограничению_типа { expr } noexcept -> type-constraint;
Требование { expr } noexcept
будет выполняться, если все функции, которые вызываются в выражении expr
, определены как noexcept
В требовании { expr } -> ограничение_типа;
после стрелки ->
должно идти ограничение типа. Например:
#include <iostream> #include <concepts> template <typename Pointer> concept is_pointer = requires (Pointer ptr, int n) { {ptr[n]}; { ptr - n } -> std::same_as<Pointer>; { ptr + n } -> std::same_as<Pointer>; }; int main() { std::cout << std::boolalpha << is_pointer<int> << std::endl; // false std::cout << std::boolalpha << is_pointer<int[]> << std::endl; // false std::cout << std::boolalpha << is_pointer<int*> << std::endl; // true std::cout << std::boolalpha << is_pointer<char*> << std::endl; // true }
Здесь концепт is_pointer
использует составное требование, которое состоит из трех требований:
requires (Pointer ptr, int n) { {ptr[n]}; { ptr - n } -> std::same_as<Pointer>; { ptr + n } -> std::same_as<Pointer>; };
Требрвание ptr[n]
проверяет, можем ли мы обращаться к значениям, применив операцию индексации. Далее применяются ряд требований с ограничением типа:
{ ptr - n } -> std::same_as<Pointer>;
В данном случае, с одной стороны, для типа Pointer должна поддерживаться операция вычитания, причем вычитается целое число. С другой стороны, результат этой операции должен также
представлять данный тип Pointer. Для установки ограничения типа применяется встроенный концепт std::same_as из заголовочного файла concepts
.
Концепту с пободными ограничениями будут соответствовать, например, указатели на значения любого типа:
is_pointer<int*> // true is_pointer<char*> // true
Например, определим составное требование для ограничения шаблона:
#include <iostream> #include <concepts> template <typename T> concept sum_types = requires (T x) { {x + x} -> std::convertible_to<T>; }; template <sum_types T> T sum(T a, T b){ return a + b;} int main() { std::cout << sum(10, 3) << std::endl; // 13 std::cout << sum(10.6, 3.2) << std::endl; // 13.8 std::cout << sum(std::string("Hello "), std::string("world")) << std::endl; // Hello world }
Здесь требование предписывает, что тип T должен поддерживать операцию сложению, при этом ее результат должен представлять тип, который может быть преобразован в тип T (либо сам тип T).