Выражение requires

Последнее обновление: 31.03.2023

Выражение 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).

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850