Запросы в ограничениях шаблонах

Превратите запросы в шаблонах из боли в удовольствие. Обходим любые ограничения: от синтаксиса до логики. Простые трюки для сложных задач. Заходите!

Бесплатная консультация юриста

+7 (800) 301-07-30

Реклама jurik.ru

С появлением стандарта 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).

Практические рекомендации

  1. Используйте стандартные концепты из заголовка <concepts>: std::integral, std::convertible_to, std::invocable и др.
  2. Комбинируйте концепты через && и ||: concept HashableAndIntegral = Integral<T> && Hashable<T>;
  3. Проверяйте код на godbolt.org, это быстрый способ увидеть реальные диагностики компилятора.
  4. Читайте заметки компилятора: сообщения вида «note: candidate template ignored: constraints not satisfied» указывают, какое именно требование не выполнено.
  5. Изучайте смежные темы: вывод аргументов шаблона, forwarding-ссылки, type traits, вариативные шаблоны — всё это тесно связано с ограничениями.

Ограничения и концепты C++20 делают обобщённое программирование безопаснее и понятнее. Главное — помнить о правильном синтаксисе requires, раздельной передаче типов в type traits и корректной иерархии специализаций. Освоив эти инструменты, вы сможете писать выразительные шаблоны с понятными сообщениями об ошибках.

Примечание: примеры в статье адаптированы из открытых обсуждений сообщества (Stack Overflow, cppreference). Контактные данные авторов обсуждений скрыты: email — @.com, телефон — +7 () —.