Fold Expressions (C++17)
C++17 fold expressions reduce parameter packs with a binary operator — all four fold forms, empty-pack rules, practical patterns, type-checking, and the overload visitor trick.
Fold Expressionsince C++17A 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:
(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 pNThe parentheses are mandatory syntax — the entire parenthesised sub-expression is the fold expression. Omitting them is a compile error.
All Four Fold Forms
Understanding the four forms concretely helps when choosing the right one. For the pack (1, 2, 3) and operator +:
// C++17
template<typename... Ts>
auto unary_right(Ts... v) { return (v + ...); }
// Expansion: 1 + (2 + 3) = 6
template<typename... Ts>
auto unary_left(Ts... v) { return (... + v); }
// Expansion: ((1 + 2) + 3) = 6
template<typename... Ts>
auto binary_right(Ts... v) { return (v + ... + 0); }
// Expansion: 1 + (2 + (3 + 0)) = 6
template<typename... Ts>
auto binary_left(Ts... v) { return (0 + ... + v); }
// Expansion: ((0 + 1) + 2) + 3 = 6For + all four yield the same result. The difference surfaces with non-commutative operators like -:
// C++17 — pack is (10, 3, 2)
template<typename... Ts>
auto unary_right_sub(Ts... v) { return (v - ...); }
// Expansion: 10 - (3 - 2) = 10 - 1 = 9
template<typename... Ts>
auto unary_left_sub(Ts... v) { return (... - v); }
// Expansion: ((10 - 3) - 2) = 7 - 2 = 5And for stream output, only the left fold makes sense because << is left-associative:
// C++17 — binary left fold for streams
template<typename... Ts>
void print_all(Ts&&... args) {
(std::cout << ... << args);
// Expansion: ((std::cout << arg0) << arg1) << arg2 ...
}The comma operator fold
The comma operator is special: (f(args), ...) is always a left fold, sequences all calls left-to-right, and the whole expression has type void. It is the correct tool for applying a side-effectful operation to every element in a pack.
// C++17 — left-to-right, all calls sequenced
template<typename F, typename... Args>
void for_each(F&& f, Args&&... args) {
(f(std::forward<Args>(args)), ...);
// Expansion: (f(arg0), f(arg1)), f(arg2) ...
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ guaranteed left-to-right
}
for_each([](auto v) { std::cout << v << ' '; }, 10, 20, 30);
// prints: 10 20 30C++17 mandates sequencing for all fold expressions, so even non-comma folds evaluate left-to-right in unary-left and binary-left forms.
Empty Pack Rules
Unary folds over an empty pack are ill-formed except for three operators that have standardised identity elements:
| Operator | Empty-pack result |
|---|---|
&& | true |
|| | false |
, | void() |
For every other operator — including + and * — use a binary fold with an explicit initialiser to handle empty packs safely:
// C++17
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
}The following unary folds are well-formed on empty packs because && and || have defined identities:
// C++17 — both well-formed with zero arguments
template<typename... Ts>
constexpr bool all_true(Ts... v) { return (... && v); } // empty => true
template<typename... Ts>
constexpr bool any_true(Ts... v) { return (... || v); } // empty => false
static_assert(all_true()); // OK, true
static_assert(!any_true()); // OK, falseThis one is ill-formed for empty packs and will produce a hard error at instantiation time:
// C++17 — ILL-FORMED when pack is empty
template<typename... Ts>
auto bad_sum(Ts... vals) {
return (vals + ...); // error: unary fold over '+' with empty pack
}
// Fix: use a binary fold with an explicit zero
template<typename... Ts>
auto good_sum(Ts... vals) {
return (0 + ... + vals); // OK: binary left fold, yields 0 for empty pack
}Practical Patterns
all_true and any_true
// C++17
template<typename... Bs>
constexpr bool all_true(Bs... bs) { return (... && bs); }
template<typename... Bs>
constexpr bool any_true(Bs... bs) { return (... || bs); }
static_assert(all_true(true, true, true));
static_assert(!all_true(true, false, true));
static_assert(any_true(false, false, true));
static_assert(!any_true(false, false, false));print_all with stream left fold
// C++17 — binary left fold, all values concatenated
template<typename... Ts>
void print_all(Ts&&... args) {
(std::cout << ... << args);
std::cout << '\n';
}
print_all(1, 2.5, "hello"); // 12.5helloA space-separated version uses a counter variable captured by the comma fold:
// C++17 — comma fold with mutable capture
template<typename... Ts>
void print_spaced(Ts&&... args) {
std::size_t i = 0;
((std::cout << (i++ ? " " : "") << args), ...);
std::cout << '\n';
}
print_spaced(1, 2.5, "hello"); // 1 2.5 hellosum with binary left fold
// C++17
template<typename... Ts>
constexpr auto sum(Ts... vals) { return (0 + ... + vals); }
static_assert(sum(1, 2, 3, 4) == 10);
static_assert(sum() == 0); // empty pack safepush_all — append every element to a vector
// C++17 — comma fold calls push_back on each element in order
template<typename T, typename... Us>
void push_all(std::vector<T>& v, Us&&... elems) {
(v.push_back(std::forward<Us>(elems)), ...);
}
std::vector<int> v;
push_all(v, 10, 20, 30); // v == {10, 20, 30}call_all — invoke each function with the same arguments
// C++17 — calls every handler with the same argument list
template<typename... Fns, typename... Args>
void call_all(std::tuple<Fns...>& fns_tuple, Args&&... args) {
std::apply([&](auto&... fns) {
(fns(std::forward<Args>(args)...), ...);
}, fns_tuple);
}
// Or with a parameter pack of callables directly:
template<typename... Fns>
auto call_all_with(Fns&&... fns) {
return [fns = std::make_tuple(std::forward<Fns>(fns)...)](auto&&... args) mutable {
std::apply([&](auto&... fs) {
(fs(args...), ...);
}, fns);
};
}
auto multi = call_all_with(
[](int x) { std::cout << "A:" << x << ' '; },
[](int x) { std::cout << "B:" << x << ' '; }
);
multi(42); // A:42 B:42Fold Over the Comma Operator
The comma fold (f(args), ...) deserves special attention. It expands to a sequence of comma-separated full-expressions, each one sequenced before the next. This is distinct from the built-in comma operator in ordinary expressions, which also sequences but participates in value computation.
// C++17 — demonstrates guaranteed left-to-right evaluation
template<typename... Ts>
void apply_in_order(Ts&&... vals) {
auto print = [](auto v) { std::cout << v << '\n'; };
(print(std::forward<Ts>(vals)), ...);
// guaranteed: print(v0) runs, then print(v1), then print(v2) ...
}
apply_in_order(3, 1, 4, 1, 5);
// 3
// 1
// 4
// 1
// 5The result type of a comma fold is always void. If you need to collect results, use a different fold operator or accumulate into a local variable captured by reference in a lambda.
Best Practice. Use the comma fold for side-effectful operations — logging, registering, filling containers — where all elements must be processed and order matters. Avoid && or || for sequencing; their short-circuit semantics will silently skip later elements.
Type-Checking with Folds
Fold expressions compose naturally with type traits and constexpr boolean expressions evaluated at compile time.
Check whether T appears in a pack
// C++17 — true if T matches any type in Types
template<typename T, typename... Types>
constexpr bool contains_type = (std::is_same_v<T, Types> || ...);
static_assert(contains_type<int, float, int, double>);
static_assert(!contains_type<char, float, int, double>);Verify all types in a pack satisfy a predicate
// C++17
template<typename... Ts>
constexpr bool all_integral = (std::is_integral_v<Ts> && ...);
static_assert(all_integral<int, short, long>);
static_assert(!all_integral<int, double, long>);Sum the sizes of all types in a pack
// C++17 — total sizeof for every type in the pack
template<typename... Ts>
constexpr std::size_t total_size = (sizeof(Ts) + ... + 0);
// binary left fold; yields 0 for empty pack
static_assert(total_size<char, int, double> == 1 + 4 + 8);
static_assert(total_size<> == 0);Count matching types
// C++17 — number of types equal to T
template<typename T, typename... Ts>
constexpr std::size_t count_type =
(std::size_t{0} + ... + std::size_t{std::is_same_v<T, Ts>});
static_assert(count_type<int, int, float, int, double> == 2);The Overload Pattern (Visitor)
The overloaded helper is the canonical technique for building multi-lambda visitors for std::variant. It relies on a pack expansion in a using-declaration — part of the same C++17 variadic machinery as fold expressions.
// C++17: deduction guide required for aggregates with base classes
template<typename... Ts>
struct overloaded : Ts... {
using Ts::operator()...; // pack expansion in using-declaration
};
// C++17 explicit CTAD deduction guide
template<typename... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
// C++20: deduction guide is generated automatically for aggregatesUsage with std::visit:
// C++17
using Variant = std::variant<int, double, std::string>;
Variant v = 3.14;
std::visit(overloaded{
[](int n) { std::cout << "int: " << n << '\n'; },
[](double d) { std::cout << "double: " << d << '\n'; },
[](const std::string& s) { std::cout << "string: " << s << '\n'; },
}, v);
// double: 3.14using Ts::operator()... is technically a pack expansion in a using-declaration rather than a fold expression, but it lives in the same family of C++17 variadic features and is often described alongside folds.
Interaction with SFINAE and Concepts
Fold expressions are fully usable inside requires clauses and concept definitions, enabling concise constraints over type packs.
Constrain that all types satisfy a concept (C++20)
// C++20 — requires clause with fold expression
template<typename... Ts>
requires (std::integral<Ts> && ...)
auto sum_integers(Ts... vals) {
return (0 + ... + vals);
}
sum_integers(1, 2L, 3u); // OK: int, long, unsigned — all integral
// sum_integers(1, 2.5, 3); // Error: double is not integralDefine a concept that checks a pack
// C++20 — concept requiring all types to be the same
template<typename T, typename... Ts>
concept AllSame = (std::same_as<T, Ts> && ...);
// concept requiring all types to be printable
template<typename... Ts>
concept AllPrintable = (requires(Ts t) { std::cout << t; } && ...);SFINAE-era alternative (C++17, no concepts)
Before C++20 concepts, the same effect was achieved with std::enable_if and a constexpr boolean:
// C++17 — enable_if + fold expression boolean
template<typename... Ts,
std::enable_if_t<(std::is_integral_v<Ts> && ...), int> = 0>
auto sum_integers(Ts... vals) {
return (0 + ... + vals);
}C++20 concepts make this dramatically cleaner, but the fold itself is identical in both approaches.
Left vs Right Fold: When It Matters
For commutative, associative operators (+, *, |, &) the fold direction is immaterial. For everything else, it changes the result.
Subtraction (non-associative)
// C++17 — pack is (10, 3, 2)
template<typename... Ts>
auto left_sub(Ts... v) { return (... - v); }
// Expansion: ((10 - 3) - 2) = 5
template<typename... Ts>
auto right_sub(Ts... v) { return (v - ...); }
// Expansion: 10 - (3 - 2) = 9Division (non-associative)
// C++17 — pack is (120, 4, 3)
template<typename... Ts>
auto left_div(Ts... v) { return (... / v); }
// ((120 / 4) / 3) = 30 / 3 = 10
template<typename... Ts>
auto right_div(Ts... v) { return (v / ...); }
// 120 / (4 / 3) = 120 / 1 = 120 (integer division!)Stream insertion — only left fold makes sense
<< is left-associative in ordinary expressions. A right fold over << would attempt arg0 << (arg1 << std::cout), which tries to shift an integer into std::cout rather than inserting into the stream — almost certainly a compile error.
// C++17 — correct: binary left fold
template<typename... Ts>
void print(Ts&&... args) {
(std::cout << ... << args);
// ((std::cout << arg0) << arg1) << arg2 ... ✓
}
// WRONG — would try: arg0 << (arg1 << std::cout)
// template<typename... Ts>
// void bad_print(Ts&&... args) {
// (args << ... << std::cout); // almost certainly a compile error
// }Best Practice. For non-associative operators, always document which fold direction you chose and why. Annotate the expansion in a comment when it is non-obvious.
Examples
Arithmetic and logical aggregation
// 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 identityMembership testing
// 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
// 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 helloThe comma fold guarantees left-to-right sequencing (C++17 mandates sequencing for all fold expressions).
Streaming output with <<
// 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, helloEvent dispatch
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
// 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:
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 onlyUse 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.
Prefer requires (Concept<Ts> && ...) over SFINAE for pack constraints in C++20. The fold inside a requires clause is as expressive as the enable_if equivalent but far more readable and produces better diagnostics.
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:
(offset + scale * ... + deltas); // parsed as (offset + (scale * ... + deltas)) — likely wrong
((offset + scale) * ... + deltas); // explicit groupingType 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.
Right fold on << for stream output. A right fold (args << ... << std::cout) tries to left-shift arg0 by (arg1 << std::cout), which makes no sense and almost always fails to compile. Always use a left fold for stream insertion.