ядро языка — синтаксисcore language — syntax
Свёртка parameter pack оператором в одно выражение — (args + ...) вместо рекурсивного шаблона с базовым и рекурсивным случаем.Folding a parameter pack with an operator into one expression — (args + ...) instead of a recursive template with a base case and a recursive case.
Fold expression — синтаксис, который применяет бинарный оператор ко всем элементам parameter pack сразу, порождая одно выражение без ручной рекурсии. Существует четыре формы: унарная правая (args op ...), унарная левая (... op args), бинарная правая (args op ... op init) и бинарная левая (init op ... op args) — с явным начальным значением init для операторов, для которых нужен случай пустого pack.A fold expression is syntax that applies a binary operator to every element of a parameter pack at once, producing a single expression with no manual recursion. There are four forms: unary right (args op ...), unary left (... op args), binary right (args op ... op init), and binary left (init op ... op args) — with an explicit initial value init for operators that need to handle an empty pack.
template<typename T>
auto sum(T v) { return v; } // базовый случай// base case
template<typename T, typename... Rest>
auto sum(T first, Rest... rest) {
return first + sum(rest...); // рекурсивный случай// recursive case
}
// две функции ради одной операции суммирования// two functions just to sum values
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // одна строка, без рекурсии// one line, no recursion
}
Вариативные шаблоны появились ещё в C++11, но единственным способом что-то сделать со всеми элементами pack оставалась рекурсия — многословная и неестественная для простых операций.Variadic templates arrived back in C++11, but the only way to do something with every element of a pack remained recursion — verbose and unnatural for simple operations.
Суммирование, логическое И/ИЛИ по всем аргументам, конкатенация строк — любая такая операция над parameter pack требовала написать перегрузку для одного аргумента (остановка рекурсии) и перегрузку для "первый + остальные" (шаг рекурсии), хотя семантически операция — это одна строчка.Summing, ANDing/ORing all arguments, concatenating strings — any such operation over a parameter pack required writing an overload for a single argument (recursion stop) and an overload for "first + the rest" (recursion step), even though the operation is semantically a one-liner.
Каждый уровень рекурсивной инстанциации шаблона — отдельная функция, которую нужно сгенерировать и, в некоторых случаях, не заинлайнить полностью. Для pack из N аргументов рекурсивный подход порождал N инстанциаций там, где fold expression порождает одно выражение.Every level of recursive template instantiation is a separate function that has to be generated and, in some cases, not fully inlined away. For a pack of N arguments, the recursive approach produced N instantiations where a fold expression produces a single expression.
Ещё один обходной путь — распаковать pack в фиктивный std::initializer_list с помощью оператора запятая ((void)std::initializer_list<int>{(f(args), 0)...}), который работал ради побочных эффектов, а не итогового значения. Он избегал рекурсии, но был совершенно нечитаем без комментария и объяснения.Another workaround was unpacking a pack into a dummy std::initializer_list using the comma operator ((void)std::initializer_list<int>{(f(args), 0)...}), which worked for side effects rather than for a resulting value. It avoided recursion, but was completely unreadable without a comment explaining it.
Компилятор разворачивает fold expression в цепочку бинарных применений оператора, где порядок группировки определяется тем, унарная это форма или бинарная, левая или правая:The compiler expands a fold expression into a chain of binary applications of the operator, where the grouping order depends on whether it's unary or binary, left or right:
// унарная правая свёртка (args op ...):// unary right fold (args op ...):
(args + ...) // == arg1 + (arg2 + (arg3 + arg4))// == arg1 + (arg2 + (arg3 + arg4))
// унарная левая свёртка (... op args):// unary left fold (... op args):
(... + args) // == ((arg1 + arg2) + arg3) + arg4// == ((arg1 + arg2) + arg3) + arg4
// бинарная правая свёртка (args op ... op init) — с явным начальным значением:// binary right fold (args op ... op init) — with an explicit initial value:
(args + ... + 0) // == arg1 + (arg2 + (arg3 + (arg4 + 0)))// == arg1 + (arg2 + (arg3 + (arg4 + 0)))
// бинарная левая свёртка (init op ... op args):// binary left fold (init op ... op args):
(0 + ... + args) // == (((0 + arg1) + arg2) + arg3) + arg4// == (((0 + arg1) + arg2) + arg3) + arg4
Помимо арифметики, fold expression часто применяют с запятой и логическими операторами для запуска действия на каждом элементе pack без построения значения:Besides arithmetic, fold expressions are often used with the comma and logical operators to run an action on every pack element without building a value:
template<typename... Args>
void print_all(Args&&... args) {
((std::cout << args << ' '), ...); // печатает каждый аргумент, свёртка по запятой// prints each argument, comma-operator fold
}
template<typename... Args>
bool all_true(Args... args) {
return (args && ...); // логическое И по всем аргументам, короткое замыкание работает// logical AND over all arguments, short-circuiting still applies
}
Унарная свёртка (args + ...) с пустым pack — ошибка компиляции, потому что для оператора + нет разумного значения "суммы нуля элементов". Только три оператора имеют определённое поведение для пустого pack в унарной форме: && даёт true, || даёт false, оператор запятая даёт void(). Для всех остальных операторов на случай пустого pack нужна бинарная форма с явным init: (args + ... + 0).A unary fold (args + ...) with an empty pack is a compile error, because there's no sensible value for "the sum of zero elements" for operator +. Only three operators have defined behavior for an empty pack in unary form: && yields true, || yields false, and the comma operator yields void(). For every other operator, handling an empty pack requires the binary form with an explicit init: (args + ... + 0).
Синтаксически бинарная свёртка отличается от унарной только наличием второго операнда рядом с ... — (args + ...) против (args + ... + 0). Легко случайно написать унарную форму там, где нужна бинарная (и получить ошибку на пустом pack в рантайме использования шаблона), или наоборот забыть, что бинарная форма требует одного и того же оператора по обе стороны от ... — смешивать операторы в одной свёртке нельзя.Syntactically, a binary fold differs from a unary one only by having a second operand next to ... — (args + ...) versus (args + ... + 0). It's easy to accidentally write the unary form where the binary one is needed (and get an error only when the template is used with an empty pack), or forget that the binary form requires the same operator on both sides of ... — mixing operators within one fold isn't allowed.
Fold expression не ограничена встроенными операторами — если для типов pack определён перегруженный operator+ или любой другой бинарный оператор, свёртка использует его так же естественно, как для int. Это делает fold expressions полезными и для собственных типов вроде векторов или матриц, а не только для арифметики примитивов.A fold expression isn't limited to built-in operators — if an overloaded operator+ or any other binary operator is defined for the pack's types, the fold uses it just as naturally as it would for int. This makes fold expressions useful for custom types like vectors or matrices too, not only for primitive arithmetic.