Концепты

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

Начиная со стандарта C++20 в язык С++ была добавлена такая функциональность как concepts (концепты). Концепты позволяют установить ограничения для параметров шаблонов (как шаблонов функций, так и шаблонов класса).

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

template <параметры>
concept имя_концепта = ограничения;

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

Ограничения представляют условные выражения, которые возвращают значение типа bool - если параметр типа удовлетворяет условию, то возвращается true.

Простейший пример:

template <typename T>
concept size = sizeof(T) <= sizeof(int);

В данном случае определен концепт size. Его смысл в том, что тип, который будет передаваться через параметр T, должен удовлетворять условию sizeof(T) <= sizeof(int). То есть физический размер объектов типа T не должен быть больше размера значений типа int.

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

имя_концепта<значения_для_параметров>

Например, проверим в действии вышеопределенный концепт size:

#include <iostream>

template <typename T>
concept size = sizeof(T) <= sizeof(int);

int main()
{
    std::cout << std::boolalpha << size<unsigned int> << std::endl;      // true
    std::cout << std::boolalpha << size<char> << std::endl;     // true
    std::cout << std::boolalpha << size<double> << std::endl;   // false 
}

В данном случае выражения size<unsigned int> и size<char> удовлетворяют ограничению концепта, так как применяемые в них типы unsigned int и char по размеру не превосходят или меньше размера типа int. Поэтому эти выражения возвратят true. А выражение size<double> не соответствует ограничению концепта, так как размер типа double больше размера типа int, поэтому это выражение возвратит false

Одни концепты могут основываться на других концептах. Например:

#include <iostream>

template <typename T>
concept small_size = sizeof(T) < sizeof(int);
template <typename T>
concept big_size = sizeof(T) > sizeof(long);

template <typename T>
concept size = small_size<T> || big_size<T>;

int main()
{
    std::cout << std::boolalpha << size<unsigned int> << std::endl; // false
    std::cout << std::boolalpha << size<char> << std::endl;         // true
    std::cout << std::boolalpha << size<double> << std::endl;       // true
}

Вначале определяется два простейших концепта. small_size требует, чтобы размер типа был меньше размера типа int. А концепт big_size требует, чтобы размер типа был больше размера типа long. С помощью стандартных логических операций && и || монжно объединять ограничения. И в данном случае концепт size требует, что тип T удовлетворял либо условию small_size<T>, либо big_size<T>:

concept size = small_size<T> || big_size<T>;

Теперь главный вопрос - зачем все эти концепты нужны? Мы можем применять концепты в качестве ограничений для шаблонов:

#include <iostream>

template <typename T>
concept size = sizeof(T) <= sizeof(int);

template <typename T> requires size<T> // в качестве ограничения применяется size
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.7) << std::endl; // ! Ошибка
}

В данном случае определен концепт size, согласно которому размер типа T должен быть равен или меньше размера типа int.

При определении шаблона функции sum в качестве ограничения применяется концепт size:

template <typename T> requires size<T>

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

sum(10, 3)

Но мы не можем передать значения типа double, так как эти значения занимают в памяти 8 байт - больше чем значения типа int:

sum(10.6, 3.7)

Поэтому последняя строка в коде закомментрирована. И если бы мы расскоментировали ее, то на этапе компиляции получили бы ошибку.

Сокращенный синтаксис

С++ позволяет сократить синтаксис применения концепта:

template <size T> // в качестве ограничения применяется size
T sum(T a, T b){ return a + b;}

В таком случае концепт указывается в угловых скобках вместа слова typename перед названием параметра типа

Встроенные концепты

Данные концепты не имеют большого смысла и призваны дать общее понимание того, как определяются и работают концепты. Кроме того, в стандартной библиотеке C++ есть довольно большой набор встроенных концептов, которые можно использовать в самых различных ситуациях. Все эти концепты определены в модуле <concepts>

Например, встроенный концеп std::same_as<K, T> проверяет, представляют ли T и K один и тот же тип. Например, нам надо определить шаблон функции, которая складывает числа int и double:

#include <iostream>
#include <concepts>

template <typename T>
concept sum_types = std::same_as<T, int> || std::same_as<T, double>;

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::same_as<T, int> || std::same_as<T, double>

устанавливает, что тип T должен представлять либо int, либо double.

Но в данном случае мы могли бы также использовать и другой концепт - std::convertible_to<K, T>. Он проверяет, можно ли преобразовать значение K в значение T. Например, значение int можно быть неявно преобразовано в double. И мы могли бы переписать предыдущий пример следующим образом:

#include <iostream>
#include <concepts>

template <typename T>
concept sum_types = std::convertible_to<T, double>; // T должен поддерживать преобразование в double

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
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850