Fold Expressions (C++17)
A compact syntax for reducing a parameter pack with a binary operator — replacing the two-function recursion pattern with a single expression. Available since C++17, fold expressions are used throughout the standard library and any code that works with variadic templates.
Before fold expressions
Before C++17, reducing a parameter pack required two function templates: a base-case overload for the single-element situation and a recursive overload that peeled off one element at a time. This is mechanically correct but has practical costs: the base-case overload is visible to the rest of the translation unit, both functions are instantiated separately for every arity, and the intent — "add all these together" — is buried in scaffolding.
// Pre-C++17: two functions required
template <typename T>
T sum(T a) { return a; } // base case (leaked into outer scope)
template <typename T, typename... Args>
T sum(T a, Args... args) { // recursive case
return a + sum(args...);
}
sum(1, 2, 3, 4, 5); // → 15
// Generates: sum<int,int,int,int,int>, sum<int,int,int,int>,
// sum<int,int,int>, sum<int,int>, sum<int> — five instantiationsC++17 fold expressions collapse this to a single expression inside a single function. The compiler generates the same addition chain, but you write it in one line and there is no leaked base-case overload.
// C++17: single function, no base-case overload
template <typename... T>
auto sum(T... args) { return (... + args); }
sum(1, 2, 3, 4, 5); // → 15, expands to ((((1+2)+3)+4)+5)The four fold forms
A fold expression is always wrapped in parentheses. The position of the ...determines the associativity (left vs. right), and the presence of an initializer distinguishes unary from binary folds. The pack is any expression containing an unexpanded parameter pack; it does not have to be just the bare pack name.
| Form | Written as | Expands to (for a, b, c) |
|---|---|---|
| Unary right fold | (pack op ...) | a op (b op c) |
| Unary left fold | (... op pack) | (a op b) op c |
| Binary right fold | (pack op ... op init) | a op (b op (c op init)) |
| Binary left fold | (init op ... op pack) | ((init op a) op b) op c |
A helpful mnemonic: the ... sits on the side toward which the fold opens. Left fold: (... op pack) — the dots are on the left, grouping accumulates leftward. Right fold: (pack op ...) — the dots are on the right, grouping accumulates rightward.
template <typename... T>
auto sub_left(T... args) { return (... - args); } // ((1-2)-3) = -4
template <typename... T>
auto sub_right(T... args) { return (args - ...); } // (1-(2-3)) = 2
sub_left(1, 2, 3); // → -4 (left-associative, like most subtraction)
sub_right(1, 2, 3); // → 2 (right-associative)
// For commutative operators (+ * && ||) associativity rarely matters,
// but for subtraction, division, and stream insertion it matters a lotSupported operators
Fold expressions support 32 binary operators. They include all the arithmetic and bitwise operators, comparisons, logical operators, the comma operator, stream insertion/extraction, pointer-to-member, and compound assignment. You cannot use non-operator syntax (e.g., a ternary or function call is not a fold operator).
Arithmetic
+ - * / %
Bitwise
& | ^ ~ << >>
Logical
&& ||
Comparison
== != < > <= >=
Assignment
= += -= *= /= %= &= |= ^= <<= >>=
Other
comma ,* ->*
The comma fold ((expr, ...)) is special: it evaluates each element of the pack as a discarded-value expression in sequence, left to right. This is the standard way to apply a side-effecting operation to every pack element.
Empty packs and identity values
Calling a variadic function with zero arguments produces an empty pack. A unary fold over an empty pack is well-formed only for three operators — those where the language defines an identity value:
| Operator | Empty pack result | Reasoning |
|---|---|---|
| && | true | Vacuous truth — "all elements satisfy X" over an empty set is true |
| || | false | Vacuous falsity — "some element satisfies X" over empty set is false |
| , | void() | No-op — doing nothing zero times is just nothing |
For every other operator, an empty-pack unary fold is ill-formed — a compile error. The fix is to use a binary fold with an explicit identity value as the initializer:
// Unary fold: ill-formed for empty pack (+ has no built-in identity)
template <typename... T>
auto bad_sum(T... args) { return (... + args); }
bad_sum(); // COMPILE ERROR: fold of empty pack over non-identity operator
// Binary left fold: explicit identity (0 for addition)
template <typename... T>
auto good_sum(T... args) { return (0 + ... + args); }
good_sum(); // → 0 ✓
good_sum(1, 2, 3); // → 6 ✓
// Other useful identities
template <typename... T> auto product(T... args) { return (1 * ... * args); }
template <typename... T> bool all_true(T... args) { return (true && ... && args); }
// Note: (true && ... && args) is a binary fold, but (args && ...) is fine too
// because && has a defined empty-pack result of trueFolding over expressions, not just pack names
The operand in a fold does not have to be the bare pack name — it can be any expression that contains the pack. This is where fold expressions become genuinely powerful: the fold applies the same operator to a whole expression pattern repeated for each element. The key insight is that the compiler substitutes each pack element into the pattern, then applies the fold operator between all the resulting expressions.
// Transform-then-fold: sum of squares
template <typename... T>
auto sum_of_squares(T... args) { return ((args * args) + ...); }
sum_of_squares(1, 2, 3); // → (1*1) + (2*2) + (3*3) = 14
// Fold over function call results
template <typename F, typename... Args>
bool all_satisfy(F pred, Args... args) {
return (pred(args) && ...);
}
all_satisfy([](int x){ return x > 0; }, 1, 2, 3); // → true
all_satisfy([](int x){ return x > 0; }, 1, -1, 3); // → false// Stream insertion: (std::cout << args) is the pattern, comma is the fold operator
template <typename... Args>
void print_all(Args&&... args) {
((std::cout << args << ' '), ...);
std::cout << '\n';
}
print_all(1, 2.5, "hello", 'X'); // 1 2.5 hello X
// Range insertion: pattern is a member-function call
template <typename Container, typename... Elems>
void push_many(Container& c, Elems&&... elems) {
(c.push_back(std::forward<Elems>(elems)), ...);
}
std::vector<int> v;
push_many(v, 1, 2, 3, 4, 5); // v = {1,2,3,4,5}// Fold over assignment: initialize multiple variables to the same value
template <typename T, typename... Refs>
void set_all(T value, Refs&... refs) {
((refs = value), ...);
}
int a, b, c;
set_all(42, a, b, c); // a = b = c = 42
// Type trait checks: is T the same as any of the listed types?
template <typename T, typename... Types>
constexpr bool is_any_of = (std::is_same_v<T, Types> || ...);
static_assert(is_any_of<int, float, int, double>); // true
static_assert(!is_any_of<char, float, int, double>); // falseAssociativity and precedence pitfalls
Two issues catch people out. First, the direction of the fold matters for non-commutative operators. Left-fold subtraction and right-fold subtraction give different results — choose deliberately. Second, if your fold expression involves operators with lower precedence than the outer context, you may need extra parentheses.
// Subtraction: direction matters
template <typename... T> auto left_sub(T... a) { return (... - a); }
template <typename... T> auto right_sub(T... a) { return (a - ...); }
left_sub(10, 3, 2); // ((10-3)-2) = 5 ← "subtract all from first"
right_sub(10, 3, 2); // (10-(3-2)) = 9 ← rarely what you want
// Stream insertion is left-fold — which is what you want:
// ((std::cout << a) << b) << c — same as std::cout << a << b << c
template <typename... T>
void print_seq(T... args) { (std::cout << ... << args); } // left fold// Precedence trap: && binds tighter than ==
// This reads as (args && ...) not ((args == expected) && ...)
template <typename T, typename... Args>
bool all_equal_wrong(T expected, Args... args) {
return (args == expected && ...); // parsed as args == (expected && ...) ← wrong!
}
// Fix: parenthesize the pattern expression
template <typename T, typename... Args>
bool all_equal(T expected, Args... args) {
return ((args == expected) && ...); // correct: compare each, then fold &&
}
all_equal(5, 5, 5, 5); // → true
all_equal(5, 5, 4, 5); // → falsePractical patterns at a glance
Sum of pack
(0 + ... + args)Binary left fold — handles empty pack
Product of pack
(1 * ... * args)Binary left fold — identity 1
All positive
((args > 0) && ...)Unary right fold — short-circuits on first false
Any negative
((args < 0) || ...)Unary right fold — short-circuits on first true
Print with spaces
((std::cout << args << ' '), ...)Pattern fold via comma — left-to-right evaluation
Push all into vector
(v.push_back(args), ...)Comma fold over member call
Call f on each
(f(std::forward<Args>(args)), ...)Perfect-forwarding comma fold
All same type
(std::is_same_v<T, Args> && ...)Logical fold over type trait
When fold expressions are not the right tool
Fold expressions are excellent for reductions and side-effecting iterations over a pack. But some variadic tasks still require recursion or other machinery:
Accessing individual elements by index
Folds process all elements uniformly. To get element N, use std::get<N>(tuple) or the nth_type recursion pattern.
Building heterogeneous data structures
std::tuple uses recursive inheritance — a fold can't build the type chain.
Early-exit with a running accumulator
Folds apply the operator to every element. For stateful short-circuit (e.g., find-first), use recursion or std::apply.
Different operation per element
A fold applies one operator uniformly. Use index_sequence + std::get if you need different behaviour per position.