С появлением стандарта C++20 в языке появился мощный механизм ограничений (constraints) и концептов (concepts), который кардинально изменил подход к написанию обобщённого кода. В этой статье мы подробно разберём, как формулировать запросы (требования) к шаблонным параметрам, какие ошибки чаще всего допускают разработчики и как их избежать.
Зачем нужны ограничения шаблонов
До C++20 проверка пригодности типа для шаблона выполнялась либо через std::enable_if, либо через SFINAE, что приводило к громоздкому и трудночитаемому коду. Ограничения позволяют:
- явно указать, каким требованиям должен удовлетворять тип;
- получать понятные сообщения об ошибках компиляции;
- перегружать шаблоны по семантическим свойствам типов;
- документировать интерфейс шаблона прямо в коде.
Концепты и выражение requires
Концепт, это именованный набор требований. Рассмотрим классический пример BoolComparable, проверяющий, что операции сравнения двух объектов типа T возвращают тип, конвертируемый в bool:
template<typename T>
concept BoolComparable = requires(T lhs, T rhs) {
{ lhs == rhs } -> std::convertible_to<bool>;
{ lhs != rhs } -> std::convertible_to<bool>;
{ lhs < rhs } -> std::convertible_to<bool>;
{ lhs > rhs } -> std::convertible_to<bool>;
{ lhs <= rhs } -> std::convertible_to<bool>;
{ lhs >= rhs } -> std::convertible_to<bool>;
};
Здесь каждая строка внутри requires — это составное требование: выражение должно быть валидным, а его результат — удовлетворять указанному концепту.
Типичные ошибки при написании ограничений
Неправильное использование std::is_invocable
Частая ошибка — передача в std::is_invocable сигнатуры функции целиком, как в std::function:
// НЕПРАВИЛЬНО: сигнатура void(bar&) передаётся одним типом template<typename FooCallableType> concept FooFunctor = std::is_invocable_v<FooCallableType, void(bar&)>;
Компилятор выдаст ошибку вида: «called object type bool is not a function or function pointer». Правильный вариант — типы аргументов передаются раздельно:
// Проверка, что Type вызываем с аргументом Bar и возвращает void std::is_invocable_r_v<void, Type, Bar>; // Проверка вызываемости без ограничения на тип возврата std::is_invocable_v<Type, Bar>;
Специализация вместо перегрузки по ограничениям
Ещё одна распространённая проблема: разработчик пишет два разных определения шаблона с разными ограничениями и получает ошибку «constraints not satisfied». Важно помнить:
- специализация с ограничениями должна быть более ограниченной, чем первичный шаблон;
- корректная схема — объявить первичный шаблон (возможно, без определения), а затем добавить специализации для разных наборов ограничений.
// Первичный шаблон (можно оставить неопределённым)
template<typename T, typename U>
class A;
// Специализация для типов, удовлетворяющих Concept1
template<typename T, typename U>
requires Concept1<T, U>
class A<T, U> { /* ... */ };
Сокращённая форма и auto-параметры
C++20 позволяет использовать сокращённые шаблоны функций (abbreviated function templates):
void foo(std::integral auto value); // параметр с концептом void bar(std::integral auto a, auto b); // смешанная форма
При этом «придуманные» компилятором шаблонные параметры добавляются в конец списка явно объявленных параметров. Обратите внимание: параметр-концепт шаблона не может сам иметь ассоциированных ограничений (temp.constr.decl).
Практические рекомендации
- Используйте стандартные концепты из заголовка
<concepts>:std::integral,std::convertible_to,std::invocableи др. - Комбинируйте концепты через
&&и||:concept HashableAndIntegral = Integral<T> && Hashable<T>; - Проверяйте код на godbolt.org, это быстрый способ увидеть реальные диагностики компилятора.
- Читайте заметки компилятора: сообщения вида «note: candidate template ignored: constraints not satisfied» указывают, какое именно требование не выполнено.
- Изучайте смежные темы: вывод аргументов шаблона, forwarding-ссылки, type traits, вариативные шаблоны — всё это тесно связано с ограничениями.
Ограничения и концепты C++20 делают обобщённое программирование безопаснее и понятнее. Главное — помнить о правильном синтаксисе requires, раздельной передаче типов в type traits и корректной иерархии специализаций. Освоив эти инструменты, вы сможете писать выразительные шаблоны с понятными сообщениями об ошибках.
Примечание: примеры в статье адаптированы из открытых обсуждений сообщества (Stack Overflow, cppreference). Контактные данные авторов обсуждений скрыты: email — @.com, телефон — +7 () —.