Лекция 15

14.05.2025 Обновлено: 14.05.2025

До концептов ограничения на шаблонные параметры выражались через enable_if, conjunction, is_integral, void_t и подобные SFINAE-трюки. Concepts — это официальный, читаемый способ делать то же самое.

Зачем нужны Concepts

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

Преимущества перед альтернативами:

  • Проще, чем enable_if — не нужно городить SFINAE.
  • Лучше, чем if constexprif constexpr работает внутри тела функции и не влияет на разрешение перегрузок; концепты участвуют в overload resolution.
  • Понятные ошибки компиляции — компилятор сразу говорит, какое требование не выполнено.
  • Неявный SFINAE — если концепт не выполнен, перегрузка просто не рассматривается.

Синтаксис requires

Ключевое слово requires пишется перед телом функции и задаёт условие для выбора перегрузки:

// Перегрузка для указателей
template<typename T>
requires std::is_pointer_v<T>
void print(const T& value) {
    std::cout << *value << std::endl;
}

// Перегрузка для всех остальных типов
template<typename T>
void print(const T& value) {
    std::cout << value << std::endl;
}

Объявление концепта

concept — это именованное требование к типам, которое можно переиспользовать:

template<typename T, typename U>
concept Addable = requires(T a, U b) {
    a + b;   // тип должен поддерживать operator+
};

template<typename T, typename U>
requires Addable<T, U>
auto add(const T& a, const U& b) {
    return a + b;
}

int main() {
    add(1, 2);          // OK
    add(Foo{}, Foo{});  // OK, если Foo поддерживает operator+
}

Способы использования концепта

// 1. В requires-clause перед телом функции
template<typename T, typename U>
requires Addable<T, U>
auto add(const T& a, const U& b);

// 2. Прямо в списке шаблонных параметров
template<typename T, Addable<T> U>
auto add(const T& a, const U& b);

// 3. Сокращённый синтаксис с auto (без явного шаблона)
auto add(const Addable auto& a, const auto& b);

Виды требований внутри requires-выражения

1. Simple requirement (простое требование)

Просто выражение, которое должно быть синтаксически корректным. Возвращаемое значение не проверяется.

template<typename... Args>
concept Addable = requires(Args... args) {
    (args + ...);   // simple requirement: operator+ должен существовать
};

2. Nested requirement (вложенное требование)

requires-выражение внутри requires-блока — позволяет добавить булево условие:

template<class T, typename... TArgs>
constexpr bool are_all_same = std::conjunction_v<std::is_same<T, TArgs>...>;

template<typename... Args>
concept Addable = requires(Args... args) {
    (args + ...);                       // simple
    requires sizeof...(Args) > 1;       // nested: аргументов должно быть больше одного
    requires are_all_same<Args...>;     // nested: все типы должны совпадать
};

3. Compound requirement (составное требование)

Проверяет не только корректность выражения, но и тип результата и/или noexcept:

// Вспомогательный алиас для получения типа первого аргумента пакета
template<typename First, typename...>
struct first_arg { using type = First; };

template<typename... Ts>
using first_arg_t = typename first_arg<Ts...>::type;

template<typename... Args>
concept Addable = requires(Args... args) {
    (args + ...);                       // simple
    requires sizeof...(Args) > 1;       // nested
    requires are_all_same<Args...>;     // nested
    // compound: выражение не бросает исключений,
    // и результат имеет тип первого аргумента
    { (args + ...) } noexcept -> std::same_as<first_arg_t<Args...>>;
};

4. Type requirement (требование к типу)

Проверяет, что вложенный тип существует:

template<typename T>
concept HasValueType = requires {
    typename T::value_type;   // у T должен быть вложенный тип value_type
};

В примере с Addable выше type requirement явно не используется, но синтаксис именно такой: typename SomeType; внутри requires-блока.

Полный пример с вариативным шаблоном

template<typename... Args>
requires Addable<Args...>
auto add(Args&&... args) {
    return (args + ...);
}

int main() {
    add(1, 2, 3, 4);      // OK
    add(Foo{}, Foo{});    // OK, если Foo удовлетворяет Addable
}

Область применения

  • Выбор перегрузки функции в зависимости от свойств типа.
  • Ограничение шаблонных классов/функций с читаемыми ошибками.
  • Замена громоздких SFINAE-конструкций на выразительные именованные требования.