Skip to content
C++
Language
since C++17
Intermediate

Fold Expressions (C++17)

C++17 fold expressions reduce parameter packs with a binary operator — syntax, fold order, empty-pack rules, and production-ready patterns.

Fold Expressionsince C++17

A fold expression reduces a variadic parameter pack to a single value by repeatedly applying a binary operator, replacing the recursive two-overload pattern that pre-C++17 variadic templates required.

Overview

Before C++17, collapsing a parameter pack over an operator required two overloads — a base case and a recursive step — roughly doubling the template machinery for every such operation. Fold expressions eliminate that boilerplate. The compiler expands the pack inline, producing an expression tree equivalent to the manual recursion but far more readable and constexpr-friendly.

Fold expressions work with any of the 32 binary operators the standard permits in fold context, including arithmetic, logical, bitwise, comparison, comma, and shift operators. They are evaluated at compile time for constant expressions and produce no runtime overhead beyond what the final expanded expression itself costs.

Syntax

There are exactly four forms:

cpp
(pack op ...)          // unary right fold:  p0 op (p1 op (p2 op ...))
(... op pack)          // unary left fold:   ((p0 op p1) op p2) op ...
(pack op ... op init)  // binary right fold: p0 op (p1 op (... op (pN op init)))
(init op ... op pack)  // binary left fold:  ((init op p0) op p1) op ... op pN

The parentheses are mandatory syntax — the entire parenthesised sub-expression is the fold expression. Omitting them is a compile error.

Empty pack rules

Unary folds over an empty pack are ill-formed except for three operators that have standardised identity elements:

OperatorEmpty-pack result
&&true
||false
,void()

For every other operator — including + and * — use a binary fold with an explicit initialiser to handle empty packs safely:

cpp
template<typename... Ts>
auto sum(Ts... vals) {
    return (0 + ... + vals);   // binary left fold — yields 0 for empty pack
}

template<typename... Ts>
auto product(Ts... vals) {
    return (1 * ... * vals);   // binary left fold — yields 1 for empty pack
}

Examples

Arithmetic and logical aggregation

cpp
// C++17
template<typename... Ts>
constexpr auto sum(Ts... v) { return (0 + ... + v); }

template<typename... Ts>
constexpr auto product(Ts... v) { return (1 * ... * v); }

template<typename... Ts>
constexpr bool all(Ts... v) { return (v && ...); }  // true for empty pack

template<typename... Ts>
constexpr bool any(Ts... v) { return (v || ...); }  // false for empty pack

static_assert(sum(1, 2, 3, 4) == 10);
static_assert(product(2, 3, 4) == 24);
static_assert(all(true, true, true));
static_assert(!any(false, false));
static_assert(all());   // true — defined identity
static_assert(!any());  // false — defined identity

Membership testing

cpp
// C++17 — short-circuits on first match
template<typename T, typename... Candidates>
bool is_one_of(T value, Candidates... candidates) {
    return ((value == candidates) || ...);
}

static_assert(is_one_of(42, 1, 2, 42, 100));
static_assert(!is_one_of('x', 'a', 'e', 'i', 'o', 'u'));

Invoking a callable on every argument

cpp
// C++17 — comma fold; evaluation order is guaranteed left-to-right
template<typename F, typename... Args>
void for_each_arg(F&& f, Args&&... args) {
    (f(std::forward<Args>(args)), ...);
}

for_each_arg([](auto v) { std::println("{}", v); }, 1, 2.5, "hello");
// prints: 1   2.5   hello

The comma fold guarantees left-to-right sequencing (C++17 mandates sequencing for all fold expressions).

Streaming output with <<

cpp
// Binary left fold — no separator between values
template<typename... Ts>
void print_all(Ts&&... args) {
    (std::cout << ... << args);
    std::cout << '\n';
}

// With separator — thread a counter through a comma fold
template<typename... Ts>
void print_csv(Ts&&... args) {
    std::size_t i = 0;
    ((std::cout << (i++ ? ", " : "") << args), ...);
    std::cout << '\n';
}

print_all(1, 2.5, "hello");    // 12.5hello  ← no spaces
print_csv(1, 2.5, "hello");    // 1, 2.5, hello

Fold over non-commutative operators

cpp
// Left vs right fold produces different results for subtraction
template<typename... Ts>
auto left_sub(Ts... v)  { return (... - v); }   // ((v0 - v1) - v2) - ...

template<typename... Ts>
auto right_sub(Ts... v) { return (v - ...); }   // v0 - (v1 - (v2 - ...))

left_sub(10, 3, 2);    // (10 - 3) - 2 = 5
right_sub(10, 3, 2);   // 10 - (3 - 2) = 9

Choose fold direction deliberately for any non-commutative operator.

Event dispatch

cpp
template<typename Event, typename... Handlers>
void dispatch(const Event& e, Handlers&&... handlers) {
    (std::forward<Handlers>(handlers)(e), ...);  // C++17, left-to-right
}

dispatch(click_event,
    [](const auto& e) { log(e); },
    [](const auto& e) { record_metrics(e); },
    [](const auto& e) { notify_observers(e); }
);

Overloaded visitor helper

cpp
// C++17: requires explicit CTAD deduction guide
template<typename... Ts>
struct overloaded : Ts... {
    using Ts::operator()...;  // pack expansion in using-declaration, C++17
};
template<typename... Ts>
overloaded(Ts...) -> overloaded<Ts...>;  // C++17 CTAD guide

// C++20: deduction guide is implicit for aggregates with base classes

std::visit(overloaded{
    [](int n)                { std::println("int: {}", n); },
    [](double d)             { std::println("double: {}", d); },
    [](const std::string& s) { std::println("string: {}", s); },
}, my_variant);

using Ts::operator()... is a pack expansion in a using-declaration, not a fold expression, but it belongs to the same C++17 variadic template machinery.

Best Practices

Prefer binary fold over unary fold for arithmetic operators. A unary (vals + ...) is ill-formed when the pack is empty. Anchoring with an identity element — (0 + ... + vals) — makes the template safe and documents the neutral value explicitly.

Match fold direction to semantics. For commutative operations the direction is immaterial, but document your choice for non-commutative ones. A reader should not need to recall associativity rules to understand intent. For floating-point sums, prefer left fold to match the natural left-to-right accumulation users expect and to keep rounding consistent.

Mark fold-expression functions constexpr (C++17) or consteval (C++20). Fold expressions expand entirely at compile time for constant inputs. Annotating them enforces and communicates this property:

cpp
template<typename... Ts>
constexpr auto sum(Ts... vals) { return (0 + ... + vals); }  // C++17

template<typename... Ts>
consteval auto sum_ct(Ts... vals) { return (0 + ... + vals); }  // C++20 — compile-time only

Use the comma fold for side-effectful operations. It guarantees evaluation order and makes intent explicit. Avoid using && or || to sequence calls — short-circuit semantics will silently suppress later calls when an early one returns false or true.

Common Pitfalls

Forgetting the parentheses. pack + ... is a syntax error; (pack + ...) is the fold expression. The parens are syntax, not grouping.

Unary fold on an empty pack with a non-special operator. (vals * ...) with zero arguments is ill-formed. The resulting compiler diagnostic is often indirect. Always supply an initialiser or add a static_assert(sizeof...(vals) > 0) for operators without a defined identity.

<< fold with no separator. (std::cout << ... << args) concatenates values with no whitespace. This surprises almost everyone who encounters it for the first time. Use the counter-in-lambda pattern when separators are needed.

Operator precedence in the initialiser. The init expression participates in the fold directly. A compound init may need its own parentheses:

cpp
(offset + scale * ... + deltas);    // parsed as (offset + (scale * ... + deltas)) — likely wrong
((offset + scale) * ... + deltas);  // explicit grouping

Type promotion across mixed arithmetic types. (vals + ...) where vals contains both int and float silently promotes via the usual arithmetic conversions. Use a typed initialiser — (0.0 + ... + vals) or (static_cast<double>(vals) + ...) — when mixing numeric types to control the result type.

Assuming ||/&& folds always evaluate all arguments. They short-circuit. If the first element of a && fold is false, subsequent elements are not evaluated. This is correct behaviour for predicates but a bug if all elements must produce side effects.

See Also