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++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.
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:
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
// 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, helloFold over non-commutative operators
// 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) = 9Choose fold direction deliberately for any non-commutative operator.
Event 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.
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.