Skip to content
C++

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 instantiations

C++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.

FormWritten asExpands 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 lot

Supported 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:

OperatorEmpty pack resultReasoning
&&trueVacuous truth — "all elements satisfy X" over an empty set is true
||falseVacuous 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 true

Folding 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>);  // false

Associativity 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);   // → false

Practical 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.

Sign in to track progress