Перейти к содержимому

Вариадные шаблоны и складные выражения

Обработайте любое количество аргументов шаблона с помощью пакетов параметров. Мастер складывания выражений для элегантной, рекурсивной обработки пакетов.

Пакеты параметров

Шаблон variadic принимает произвольное количество аргументов шаблона с использованием пакета parameter**. Синтаксис использует эллипсис (...). Существует два вида пачек:

1. Пакет параметров шаблона — template — фиксирует аргументы нулевого или более типа.
2. Пакет параметров функции — void f(Ts... args) — фиксирует соответствующие аргументы функции.

Вы не можете напрямую повторить пакет в петле. Вместо этого вы расширяете его с помощью оператора .... Ключевое понимание заключается в том, что расширение pack применяет паттерн к каждому элементу пакета и создает отдельный список.

sizeof...(Ts) и sizeof...(args) возвращают количество элементов в пакете как constexpr std::size_t.

Расширение рекурсивных пакетов (Legacy Pattern)

До появления сгибаемых выражений C++17 стандартным способом обработки пакетов была рекурсивная инстанциация: отслаивание первого элемента, его обработка и повторение на остальных. Это требует базового случая для прекращения рекурсии.

recursive_variadic.cpp
#include <iostream>

// Base case: no arguments
void print() {
    std::cout << '\n';
}

// Recursive case: process first, recurse on rest
template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) {
        std::cout << ", ";
    }
    print(rest...);  // expand rest, recurse
}

int main() {
    print(1, 2.5, "hello", 'A');
    // Output: 1, 2.5, hello, A

    print(); // just a newline
}

Складные выражения (C++17)

C++17 fold expressions в большинстве случаев устраняет необходимость в рекурсивных шаблонах. Выражение сгиба применяет двоичный оператор ко всем элементам пакета параметров. Существует четыре формы:

Односторонняя правая складка: (pack op ...) расширяется до e1 op (e2 op (... op eN))
Универсальная левая складка: (... op pack) расширяется до ((e1 op e2) op ...) op eN
- Бинарная правая складка: (pack op ... op init) - включает начальное значение
- Бинарная левая складка: (init op ... op pack) - включает начальное значение

fold_expressions.cpp
#include <iostream>
#include <string>

// Sum all arguments — unary left fold with +
template <typename... Ts>
auto sum(Ts... args) {
    return (... + args);  // ((a1 + a2) + a3) + ...
}

// Print all arguments — binary left fold with <<
template <typename... Ts>
void print(Ts&&... args) {
    (std::cout << ... << args) << '\n';
    // expands to: ((std::cout << a1) << a2) << a3 ...
}

// Check if ALL arguments satisfy a predicate — unary left fold with &&
template <typename... Ts>
bool all_positive(Ts... args) {
    return (... && (args > 0));  // (a1>0) && (a2>0) && ...
}

// Push multiple elements to a vector — fold over comma operator
#include <vector>
template <typename T, typename... Ts>
void push_all(std::vector<T>& vec, Ts&&... args) {
    (vec.push_back(std::forward<Ts>(args)), ...);
}

int main() {
    std::cout << sum(1, 2, 3, 4, 5) << '\n'; // 15
    print("x=", 10, " y=", 20);               // x=10 y=20
    std::cout << std::boolalpha
              << all_positive(1, 2, 3)   << '\n'  // true
              << all_positive(1, -2, 3)  << '\n'; // false

    std::vector<int> v;
    push_all(v, 10, 20, 30);
    for (int x : v) std::cout << x << ' ';  // 10 20 30
    std::cout << '\n';
}

Расширение контекста

Расширение пакета (...) может появляться во многих контекстах, а не только в аргументах вызова функции:

Аргументы функции: f(args...) или f(transform(args)...)
Аргументы шаблона: std::tuple
Базовые классы: class Derived : public Bases... { };
Первоначальные списки: auto list = { args... };
Ламбда захватывает : [args...] { } или [...args = std::move(args)] { } (C++20 init-capture)
Пользовательские декларации (C++17): using Bases::operator()...;

pattern - это все, что появляется перед .... В f(transform(args)...) шаблон transform(args), который расширяется до f(transform(a1), transform(a2), transform(a3)).

Это очень важно для понимания: f(args)... - это не то же самое, что f(args...). Первый расширяется до f(a1), f(a2), f(a3) (три отдельных вызова в выражении запятой), а второй расширяется до f(a1, a2, a3) (один вызов с тремя аргументами).

std::tuple & std::apply

std::tuple - это стандартный библиотечный разнородный контейнер, построенный на вариадных шаблонах. std::apply (C++17) вызывает вызов с элементами набора, распакованными в качестве аргументов. std::index_sequence позволяет индексировать время компиляции в кортежи.

tuple_apply.cpp
#include <tuple>
#include <iostream>
#include <string>
#include <utility>

// std::apply calls a function with tuple elements as arguments
void greet(const std::string& name, int age) {
    std::cout << "Hello " << name << ", age " << age << '\n';
}

// Print all tuple elements using index_sequence
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}

template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
    std::cout << '(';
    print_tuple_impl(t, std::index_sequence_for<Ts...>{});
    std::cout << ")\n";
}

int main() {
    auto person = std::make_tuple(std::string("Alice"), 30);
    std::apply(greet, person);  // Hello Alice, age 30

    auto data = std::make_tuple(1, 3.14, std::string("hello"));
    print_tuple(data);  // (1, 3.14, hello)
}
Подводный камень

Наиболее распространенные ошибки при вариадных шаблонах:

1. Забывание ... — написание args, когда вы имеете в виду args..., оставляет пакет нерасширенным, вызывая ошибку компиляции.
2. Расширение в неправильном месте — f(g(args)...) против f(g(args...)) имеют очень разные значения. Первый вызывает g один раз на элемент; второй вызывает g со всеми элементами.
3. Пустые пакеты — (... + args) с пустым пакетом плохо сформирован для большинства операторов. Используйте двоичную складку с начальным значением: (0 + ... + args).
4. sizeof... возвращает счет , а не размер в байтах — sizeof...(args) дает количество элементов пакета, а не их общий размер памяти.

Лучшая практика

Предпочитают складывать выражения поверх рекурсивных вариадных шаблонов, когда это возможно. Они более лаконичны, легче читаются и часто компилируются быстрее. Используйте двоичные складки с явным начальным значением для безопасной обработки пустых упаковок. Резервное рекурсивное расширение для случаев, когда сгибаемых выражений недостаточно — например, когда нужно обрабатывать элементы парами, или при обработке каждого элемента требуется разная логика.

Ключевые выводы
  • Различные шаблоны используют typename... Ts (пакет параметров шаблона) и Ts... args (пакет параметров функции)
  • sizeof...(args) возвращает количество элементов в пакете во время компиляции
  • Расширение пакета применяет шаблон для каждого элемента: f(transform(args)...) вызывает преобразование на каждом элементе.
  • Складные выражения (C++17) заменяют рекурсивное расширение: (... + args) суммирует все элементы
  • Используйте бинарные складки с начальным значением для безопасной обработки пустых пакетов
  • std::apply распаковывает набор в качестве аргументов функции; std::index_sequence позволяет индексировать время компиляции

Тест — проверьте свои знания

(20 XP)

1. Что такое `(... + args)` для `args = {a, b, c}`?

2. В чем разница между `f(args...)` и `f(args)...`?

3. Что такое `sizeof...(args)`?