Лекция 11

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

Вариативные шаблоны

В C++11 — рекурсивный вызов функции с уменьшающимся количеством аргументов. На примере функции to_strings:

template<typename T, typename... Args>
void to_strings(T value, Args... args) {
    // махинации с value
    to_strings(args...);   // вызов функции от всех аргументов, кроме T value
}

По факту мы каждый раз преобразовываем только value, а потом запускаем эту же функцию от всех аргументов, кроме value. Тогда первый аргумент из args становится value. И так далее. Нужна базовая функция от 0 аргументов (или специализация на одном аргументе) — это не очень удобно.

Parameter pack: возможности

  • Сам ... — оператор «развёртки» пакета.
  • Модификаторы const/volatile работают с пакетом обычно.
  • sizeof...(Args)количество элементов в пакете (а не их суммарный размер).
  • fold-expression — компактная свёртка пакета через бинарный оператор.

Где может использоваться parameter pack:

  • Параметр шаблона — 0..n шаблонных аргументов.
  • Аргументы функции — 0..n аргументов.
  • В if constexpr.
  • В sizeof... — количество элементов в пакете.
  • При вызове функции через f(args...) — она получит все аргументы пакета.

Fold-expression (C++17)

Применяет бинарный оператор ко всем элементам пакета одним выражением, без рекурсии.

template<typename... Args>
auto sum(Args... args) {
    return (args + ...);     // унарная fold справа
}

Виды fold:

ЗаписьРаскрытие
(args op ...)args0 op (args1 op (args2 op args3)) — правая
(... op args)((args0 op args1) op args2) op args3 — левая
(args op ... op init)args0 op (args1 op (args2 op init)) — правая с инитом
(init op ... op args)((init op args0) op args1) op args2 — левая с инитом

Применимо и для унарных, и для бинарных операторов.

Разная расстановка скобок важна для неассоциативных операций:

  • Правая: (args / ...)8 / (4 / 2) = 4
  • Левая: (... / args)(8 / 4) / 2 = 1

Comma fold pattern

(func(args), ...) — последовательность вызовов: func(arg0), func(arg1), func(arg2), …

Удобно, когда хочется выполнить действие для каждого элемента пакета.

template<typename... Args>
void print_all(Args... args) {
    ((std::cout << args << ' '), ...);
}

Класс с переменным числом параметров — std::tuple

Главный пример вариативного шаблонного класса. Наивная реализация — через рекурсию-наследование:

// Базовый случай — 0 аргументов
template<typename... Ts>
struct tuple {};

// Специализация: голова + хвост
template<typename Head, typename... Tail>
struct tuple<Head, Tail...> : tuple<Tail...> {
    Head value;

    tuple(Head h, Tail... t)
        : tuple<Tail...>(t...), value(h)
    {}
};
  • Класс наследуется от tuple с меньшим числом аргументов.
  • В конструкторе инициализируем своё поле и базовый класс.
  • Чтобы получить более глубокие поля, можно static_cast структуры к её базовому классу.
  • Альтернатива — хранить поле base (или ссылку на него) и получать значения как t.base.base.value.
  • Для геттера по индексу делаем шаблонный аргумент-индекс и using по нему; нужна также специализация для Index == 0.

(Полная реализация std::tuple — на следующей лекции.)