Вариадные шаблоны и складные выражения
Обработайте любое количество аргументов шаблона с помощью пакетов параметров. Мастер складывания выражений для элегантной, рекурсивной обработки пакетов.
Пакеты параметров
Шаблон variadic принимает произвольное количество аргументов шаблона с использованием пакета parameter**. Синтаксис использует эллипсис (...). Существует два вида пачек:
1. Пакет параметров шаблона — template — фиксирует аргументы нулевого или более типа.
2. Пакет параметров функции — void f(Ts... args) — фиксирует соответствующие аргументы функции.
Вы не можете напрямую повторить пакет в петле. Вместо этого вы расширяете его с помощью оператора .... Ключевое понимание заключается в том, что расширение pack применяет паттерн к каждому элементу пакета и создает отдельный список.
sizeof...(Ts) и sizeof...(args) возвращают количество элементов в пакете как constexpr std::size_t.
Расширение рекурсивных пакетов (Legacy Pattern)
До появления сгибаемых выражений C++17 стандартным способом обработки пакетов была рекурсивная инстанциация: отслаивание первого элемента, его обработка и повторение на остальных. Это требует базового случая для прекращения рекурсии.
#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) - включает начальное значение
#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 позволяет индексировать время компиляции в кортежи.
#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)`?