Введение
Одна из главных болей шаблонного программирования в C++ — нечитаемые сообщения об ошибках, когда шаблон используется с неподходящим типом. До C++20 единственным способом ограничить допустимые типы и улучшить диагностику было использование SFINAE (Substitution Failure Is Not An Error) — техники, формально работающей, но крайне громоздкой и непонятной для новичков. C++20 вводит концепты (concepts) — именованные, явно читаемые ограничения на шаблонные параметры.
Суть концепции
Концепт — это предикат компиляции: набор требований к типу (наличие операций, других концептов, выражений), который можно проверить и которому можно дать осмысленное имя. Если тип не удовлетворяет концепту, компилятор сразу укажет, какое именно требование не выполнено, вместо того чтобы выводить стену текста из недр инстанцирования шаблона.
#include <concepts>
#include <iostream>
#include <string>
// Собственный концепт: тип поддерживает оператор < и оператор +
template <typename T>
concept Addable = requires(T a, T b) {
a + b;
};
template <typename T>
concept Comparable = requires(T a, T b) {
a < b;
};
// Используем концепт как ограничение на параметр шаблона
template <Comparable T>
T myMax(T a, T b)
{
return (a < b) ? b : a;
}
// Альтернативный синтаксис с requires-выражением
template <typename T>
requires Addable<T> && Comparable<T>
T sumIfGreater(T a, T b)
{
if (a < b) {
return a + b;
}
return a;
}
// Стандартные концепты из <concepts>
template <std::integral T>
T square(T value)
{
return value * value;
}
int main()
{
std::cout << myMax(3, 7) << std::endl;
std::cout << myMax(std::string("abc"), std::string("abd")) << std::endl;
std::cout << sumIfGreater(2, 5) << std::endl;
std::cout << square(6) << std::endl; // OK: int — integral
// square(3.14); // ОШИБКА КОМПИЛЯЦИИ: double не удовлетворяет std::integral
// Сообщение об ошибке прямо укажет на нарушенное требование концепта.
return 0;
}
Разбор примера
Addable и Comparable — собственные концепты, описанные через requires-выражение: внутри фигурных скобок мы просто пишем код, который должен «уметь скомпилироваться» для типа T. Если выражение a + b или a < b не компилируется для конкретного типа, концепт считается не удовлетворённым.
template <Comparable T> T myMax(...) — это сокращённый синтаксис: концепт используется прямо вместо typename в списке параметров шаблона. Эквивалентная, более явная форма — template <typename T> requires Comparable<T>.
std::integral — один из стандартных концептов из заголовка <concepts>, который проверяет, является ли тип целочисленным (int, long, char, bool и т. д.). Если попытаться вызвать square(3.14), компилятор немедленно сообщит, что double не удовлетворяет std::integral, указав на конкретное несоответствие, а не печатая десятки строк инстанцирования шаблонов, как было бы с обычным template <typename T> без концептов.
Где использовать концепты
- Документирование интерфейса шаблона. Имя концепта (
Comparable,Addable,std::ranges::range,std::invocable) само по себе объясняет, что требуется от типа, не заглядывая в реализацию функции. - Перегрузка по семантике типа. Можно определить несколько версий функции для разных категорий типов и позволить компилятору выбрать подходящую через
requires-ограничения — это явная и читаемая альтернатива SFINAE и тегам-диспетчерам. - Constrained
auto. Концепты можно использовать прямо при объявлении переменных и параметров:void process(std::integral auto value)— это сокращённая форма шаблонной функции с ограничением.
void process(std::integral auto value)
{
std::cout << "Целое число: " << value << std::endl;
}
Подводные камни и советы
- Концепт — это не runtime-проверка, вся проверка происходит во время компиляции. Не пытайтесь использовать концепты для валидации пользовательского ввода в runtime.
- Не переусложняйте собственные концепты. Если в стандартной библиотеке уже есть подходящий концепт (
std::integral,std::floating_point,std::same_as,std::derived_from,std::invocable,std::ranges::rangeи десятки других) — используйте его вместо изобретения своего. requires-выражение иrequires-предложение — разные вещи.requires(T a) { ... }внутриconcept— это requires-выражение (проверка компилируемости кода).requires Comparable<T>после списка шаблонных параметров — это requires-предложение (накладывает ограничение). Их легко спутать по синтаксису, но назначение разное.- Композиция концептов через
&&и||работает естественно:requires Addable<T> && Comparable<T>— тип должен удовлетворять обоим требованиям. - Концепты особенно ценны в сочетании с шаблонами классов и фабричными функциями — ограничения можно накладывать как на сам класс, так и на отдельные его методы независимо.
Концепты делают шаблонное программирование в C++ значительно более доступным новичкам: вместо изучения SFINAE-трюков можно явно и читаемо сформулировать, что требуется от типа, и получить понятную ошибку компиляции при нарушении этого требования.
================================================================================