C++17 Fold Expressions: Unary and Binary Folds Instead of Recursive Templates

Key takeaways

C++17 Fold Expressions. Handling parameter packs, unary/binary folding.

What are Fold Expressions?

C++17 fold expressions are a syntax for “folding” a parameter pack in variadic templates using an operator. Without requiring recursive templates, you can apply operators like +, &&, <<, etc., to the entire pack at once. This can make your code more concise, especially after mastering template basics.

// Before C++17: Recursion
template<typename T>
T sum(T value) {
    return value;
}

template<typename T, typename... Args>
T sum(T first, Args... args) {
    return first + sum(args...);
}

// C++17: Fold Expression
template<typename... Args>
auto sum(Args... args) {
    return (... + args);  // Unary left fold
}

cout << sum(1, 2, 3, 4, 5) << endl;  // 15

The recursive version needs two overloads — a base case (single argument) and a recursive case — because template recursion has to bottom out somewhere the compiler can resolve without infinite instantiation; every call to variadic sum peels off one argument and recurses until only the base-case overload matches. The fold expression replaces that entire two-function structure with a single expression the compiler expands directly into the equivalent chain of + operations at compile time — no actual runtime recursion, no separate base-case overload to maintain, and the generated code is typically identical to what you’d get from hand-unrolling the recursive version yourself.

4 Types of Folding

// 1. Unary right fold: (args op ...)
template<typename... Args>
auto sum1(Args... args) {
    return (args + ...);  // ((1 + 2) + 3) + 4
}

// 2. Unary left fold: (... op args)
template<typename... Args>
auto sum2(Args... args) {
    return (... + args);  // 1 + (2 + (3 + 4))
}

// 3. Binary right fold: (args op ... op init)
template<typename... Args>
auto sum3(Args... args) {
    return (args + ... + 0);  // ((1 + 2) + 3) + 0
}

// 4. Binary left fold: (init op ... op args)
template<typename... Args>
auto sum4(Args... args) {
    return (0 + ... + args);  // 0 + (1 + (2 + 3))
}

The ...’s position relative to the operator is what determines associativity, and getting this backwards is a genuinely easy mistake since the four forms look superficially similar: ... on the left of the operator means the pack expands to the left, associating right-to-left (1 + (2 + (3 + 4))); ... on the right associates left-to-right (((1 + 2) + 3) + 4). For an associative, commutative operator like integer +, all four forms produce the same numeric result, which is why the difference is easy to overlook — but it stops being cosmetic the moment the operator isn’t associative (subtraction, << stream insertion, matrix multiplication), where left and right folds genuinely compute different things. The binary forms (with an explicit init value) exist specifically to handle the empty-pack case safely, covered in the Common Issues section below.

Supported Operators

// Arithmetic operators
(... + args)   // Addition
(... - args)   // Subtraction
(... * args)   // Multiplication
(... / args)   // Division

// Logical operators
(... && args)  // AND
(... || args)  // OR

// Bitwise operators
(... & args)   // AND
(... | args)   // OR
(... ^ args)   // XOR

// Comparison operators
(... < args)   // Less than
(... > args)   // Greater than

// Miscellaneous
(... , args)   // Comma

Not every C++ operator can appear in a fold expression — the standard restricts it to a fixed list of roughly 30 binary operators (the arithmetic, logical, bitwise, comparison, assignment, and comma operators shown above, plus a few more like ->* and .*), which is a deliberate constraint rather than an oversight: fold expressions were designed around operators with well-understood associativity and a sensible “fold” semantics, not as a general mechanism for arbitrary user-defined operator overloads to hook into. If you overload operator+ for a custom type, though, that overload is what a fold expression calls — fold expressions dispatch to whatever operator+ resolves to via normal overload resolution, custom types included.

Practical Examples

Example 1: Printing

template<typename... Args>
void print(Args... args) {
    (cout << ... << args) << endl;
}

print("x = ", 42, ", y = ", 3.14);
// x = 42, y = 3.14

This is the fold expression pattern that most surprises people the first time they see it, because << isn’t the arithmetic or logical operator these expressions are usually introduced with — but std::cout << args is just an ordinary binary operator call under the hood (returning the stream, so it chains), and fold expressions don’t care what the operator’s semantic meaning is, only that it’s one of the supported binary operators. Folding << this way is the idiomatic C++17 replacement for what used to require a recursive variadic print function, and it’s genuinely one of the most common real-world uses of fold expressions in production code (logging helpers, debug-printing utilities).

Example 2: Checking All Values

template<typename... Args>
bool all(Args... args) {
    return (... && args);
}

cout << all(true, true, true) << endl;   // 1
cout << all(true, false, true) << endl;  // 0

&& folds short-circuit exactly the way a hand-written chain of && would — the compiler-generated expansion is args[0] && args[1] && args[2] && ..., so evaluation stops at the first false rather than needlessly evaluating every remaining argument. This matters if any argument is an expression with a side effect or a non-trivial cost to evaluate, not just a plain boolean variable: the fold gets the same short-circuit efficiency as manually-written boolean logic, for free.

Example 3: Adding to a Container

template<typename T, typename... Args>
void push_back_all(vector<T>& vec, Args... args) {
    (vec.push_back(args), ...);
}

vector<int> vec;
push_back_all(vec, 1, 2, 3, 4, 5);
// vec = {1, 2, 3, 4, 5}

This is the comma-operator fold, and it’s the pattern worth remembering specifically for cases where you want to run an expression for its side effect on each pack element rather than fold the pack into a combined value. (vec.push_back(args), ...) expands to vec.push_back(args[0]), vec.push_back(args[1]), vec.push_back(args[2]), ... — the comma operator evaluates each push_back call in sequence and discards each individual result, which is exactly what you want here since push_back returns void and there’s nothing to combine.

Example 4: Calling Functions

template<typename... Funcs>
void call_all(Funcs... funcs) {
    (funcs(), ...);
}

void f1() { cout << "f1" << endl; }
void f2() { cout << "f2" << endl; }
void f3() { cout << "f3" << endl; }

call_all(f1, f2, f3);
// f1
// f2
// f3

The same comma-fold pattern as push_back_all, but folding over a pack of callables rather than a pack of values passed to a fixed function — funcs here is a pack of function objects, and (funcs(), ...) invokes each one in pack order. This generalizes naturally to visitor-style patterns: a pack of lambdas, each handling a different case, invoked in sequence over some shared input.

Example 5: Range Checking

template<typename T, typename... Args>
bool in_range(T value, T min, T max, Args... args) {
    return ((value >= min && value <= max) || ... || 
            (value >= args && value <= args));
}

// Or simplified
template<typename T, typename... Args>
bool contains(T value, Args... args) {
    return ((value == args) || ...);
}

cout << contains(3, 1, 2, 3, 4, 5) << endl;  // 1
cout << contains(6, 1, 2, 3, 4, 5) << endl;  // 0

contains composes two folds in one expression: the inner (value == args) isn’t a fold at all (it’s an ordinary comparison, evaluated once per expanded pack element), while the outer (... || ...) is what folds those individual comparison results together with short-circuiting OR — this two-layer structure (an expression involving the pack, folded with a combining operator) is the general template for most non-trivial fold-expression usage, not just simple “apply one operator to raw pack elements.”

Example 6: Minimum/Maximum Value

template<typename... Args>
auto min(Args... args) {
    return (args < ...);  // Incorrect!
    // This is the classic fold-expression trap worth understanding, not
    // just avoiding: `<` isn't associative or transitive the way `+` is,
    // so folding it doesn't compute "the minimum" — it produces a chain of
    // boolean comparisons that collapses to a single true/false, not a
    // value from the original pack at all. For min(5, 2, 8), this expands
    // to 5 < (2 < 8), and since 2 < 8 evaluates to the bool true (which
    // implicitly converts to 1), that's really 5 < 1 — a boolean result
    // wearing the return type of whatever `args` happened to be, with no
    // relationship to the actual smallest argument. The compiler accepts
    // this silently because it's syntactically valid; it's a logic bug,
    // not a compile error, which is exactly what makes it dangerous.
}

// Correct implementation
template<typename T>
T min(T value) {
    return value;
}

template<typename T, typename... Args>
T min(T first, Args... args) {
    T rest = min(args...);
    return first < rest ? first : rest;
}

// Or using std::min
template<typename... Args>
auto minimum(Args... args) {
    return std::min({args...});
}

cout << minimum(5, 2, 8, 1, 9) << endl;  // 1

The takeaway worth generalizing from this example: a fold expression is the right tool when the operator you’re folding is genuinely associative in the mathematical sense (+, *, &&, ||) — for anything that needs to track “which element” rather than just combine values pairwise (min, max, argmax), reach for std::min/std::max with a brace-initializer over the pack, or fall back to recursion, rather than trying to force it through a fold.

Comma Operator

template<typename... Args>
void process(Args... args) {
    int dummy[] = {(cout << args << " ", 0)...};
}

process(1, 2, 3, 4, 5);
// 1 2 3 4 5

// This is the pre-C++17 trick fold expressions were partly introduced to
// retire: `int dummy[] = {(expr, 0)...}` abuses brace-init-list pack
// expansion to force each `expr` to evaluate once per pack element, using
// the comma operator to discard `expr`'s (often void) result and produce
// the `0` the array actually needs as a filler value. It works, but it's
// indirect and easy to misread; the fold-expression version below does
// exactly the same thing far more directly and needs no dummy array at all.

// Or using Fold Expression
template<typename... Args>
void process2(Args... args) {
    ((cout << args << " "), ...);
}

Common Issues

Issue 1: Empty Pack

This is arguably the single most important pitfall in this entire guide, because unlike the min/max mistake above, this one fails at compile time with an error message that doesn’t always make it obvious what went wrong — a unary fold called with zero pack elements is ill-formed for every operator except &&, ||, and the comma operator, each of which has a sensible mathematical identity value the standard defines specifically to handle this case.

// ❌ Empty pack (only some operators allowed)
template<typename... Args>
auto sum(Args... args) {
    return (... + args);  // Error if pack is empty
}

// ✅ Provide an initial value
template<typename... Args>
auto sum(Args... args) {
    return (0 + ... + args);  // 0 if pack is empty
}

// Operators that allow empty packs: &&, ||, ,
(... && args)  // true
(... || args)  // false
(... , args)   // void()

sum(0 + ... + args) works for an empty pack not through special-casing but because it’s a binary fold — the explicit 0 is what the expansion collapses to when there’s nothing left in the pack to combine with it, so it never hits the ill-formed unary-empty-pack case at all. This is the practical rule of thumb: if a fold might ever be called with zero arguments (which is common for variadic utility functions meant to be maximally generic), prefer the binary form with an explicit identity value over the unary form, unless the operator is one of the three with a built-in empty-pack meaning.

Issue 2: Operator Precedence

// ❌ Confusing precedence
template<typename... Args>
auto func(Args... args) {
    return (... + args * 2);  // Error
}

// ✅ Explicit parentheses
template<typename... Args>
auto func(Args... args) {
    return (... + (args * 2));
}

The failing version isn’t ambiguous to a human reading “add up each arg times two,” but the compiler parses (... + args * 2) strictly by C++‘s normal operator precedence rules first, then tries to fit the fold’s ... syntax around that — and * binding tighter than the fold’s + produces a shape the fold-expression grammar doesn’t recognize as valid at all, hence a parse error rather than a silently wrong result like the min/max case earlier. The fix is mechanical: always wrap the per-element expression being folded in its own parentheses, which removes any ambiguity between where the fold operator ends and the per-element expression begins.

Issue 3: Type Mismatch

// ❌ Type mismatch
auto x = (1 + ... + 3.14);  // int + double

// ✅ Explicit type
template<typename T, typename... Args>
T sum(Args... args) {
    return (T(0) + ... + T(args));
}

1 + ... + 3.14 involving mixed int and double pack elements doesn’t fail because fold expressions themselves are type-strict — it fails (or silently produces a surprising result) for the same reason 1 + 3.14 in ordinary code triggers usual arithmetic conversions per-operation, so a mixed-type pack folded without an explicit common type can produce a result whose type depends on the pack’s order, not just its contents. Explicitly casting every element to a single template type T before folding, as shown, forces a single unambiguous type for the entire computation instead of letting each intermediate step’s type be decided implicitly by whatever two operands happen to be combined at that step.

Fold vs Recursion

// Recursion (complex)
template<typename T>
void print(T value) {
    cout << value << endl;
}

template<typename T, typename... Args>
void print(T first, Args... args) {
    cout << first << " ";
    print(args...);
}

// Fold (simple)
template<typename... Args>
void print(Args... args) {
    ((cout << args << " "), ...);
    cout << endl;
}

Beyond brevity, fold expressions typically compile faster than the equivalent recursive template — each level of template recursion is a separate template instantiation the compiler has to generate and process, while a fold expression is expanded directly by the compiler without going through that instantiation machinery at all. On a codebase with many variadic utilities, this is a real, measurable difference in build times, not just a stylistic preference — though the FAQ below is right that it’s a secondary benefit; readability and eliminating the base-case boilerplate is usually the bigger win.

FAQ

Q1: When should I use Fold Expressions?

A:

  • When working with variadic templates
  • To handle parameter packs
  • To replace recursion

Q2: Are all operators supported?

A: Most binary operators are supported. Unary operators are not.

Q3: What about performance?

A: Performance is similar to recursion, but compile time can be shorter.

Q4: What about empty packs?

A: Only &&, ||, and , are allowed. Other operators require an initial value.

Q5: What about pre-C++17?

A: Use recursive templates.

Q6: Where can I learn more about Fold Expressions?

A:

  • “C++17 The Complete Guide”
  • cppreference.com
  • “Effective Modern C++”

Related Posts: Advanced Variadic Templates, Template Basics, Template Argument Deduction.