Skip to content

Fold Expressions (C++17) — Unary and Binary Folds, Operator Packs, Compile-Time Reduction

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Fold Expressions (C++17). We cover key concepts, practical examples, and best practices to help you master this topic.

C++17 fold expressions apply binary operators directly to parameter packs — (args + ...), (... && args), (args || ...) — eliminating recursive template expansion for compile-time reductions.

What You'll Learn

You will write unary and binary fold expressions for all 32 foldable operators, understand left-fold vs right-fold evaluation order, use fold expressions with logical operators for compile-time checks, combine folds with comma operator for sequential operations, and apply folds in real-world patterns like Type Checking and container operations.

Why It Matters

Before fold expressions, reducing a parameter pack to a single value required recursive templates — verbose, error-prone, and slow to compile. A fold expression does the same work in one line. They are essential in variadic template code, powering everything from std::apply to custom tuple implementations. C++ libraries rely on folds for concise, high-performance variadic operations.

Learning Path

graph LR
    A["55: if/switch init + if constexpr"] --> B["56: Fold Expressions"]
    B --> C["57: Coroutines"]
    C --> D["58: Modules"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Unary Fold Syntax

There are four unary fold forms:

Form Syntax Expansion (for args = a, b, c)
Right fold (args op ...) (a op (b op c))
Left fold (... op args) ((a op b) op c)
Right fold (empty) (args op ...) Error with empty pack if op requires value
Left fold (empty) (... op args) Error with empty pack if op requires value
#include <iostream>
#include <string>

// Unary right fold: (args + ...) → (a + (b + (c + ...)))
template <typename... Args>
auto sumRight(Args... args) {
    return (args + ...);
}

// Unary left fold: (... + args) → (((a + b) + c) + ...)
template <typename... Args>
auto sumLeft(Args... args) {
    return (... + args);
}

int main() {
    // For + (associative), both produce the same result
    std::cout << sumRight(1, 2, 3, 4, 5) << "\n";  // 15
    std::cout << sumLeft(1, 2, 3, 4, 5) << "\n";   // 15

    // For non-associative operators, order matters
    // Right fold: (1 - (2 - (3 - 4))) = 1 - (2 - (3 - 4))
    //                                     = 1 - (2 - (-1))
    //                                     = 1 - 3 = -2
    // Left fold:  (((1 - 2) - 3) - 4) = -8
}

Binary Fold Syntax

Binary folds provide an identity value for empty packs:

Form Syntax Expansion
Right binary (args op ... op init) (a op (b op (c op init)))
Left binary (init op ... op args) (((init op a) op b) op c)
#include <iostream>
#include <string>

// Binary fold with identity value
template <typename... Args>
auto sumWithDefault(Args... args) {
    return (args + ... + 0);  // 0 is the identity for addition
}

// String concatenation with identity
template <typename... Args>
std::string concat(Args... args) {
    return (args + ... + std::string{});  // Empty string identity
}

int main() {
    // With binary fold, empty pack is valid
    std::cout << sumWithDefault() << "\n";       // 0 (empty pack)
    std::cout << sumWithDefault(1, 2, 3) << "\n"; // 6

    std::cout << concat() << "\n";                       // (empty string)
    std::cout << concat("hello", " ", "world") << "\n";  // hello world

    // Multiplication with identity 1
    auto product = [](auto... args) {
        return (args * ... * 1);
    };
    std::cout << product() << "\n";       // 1
    std::cout << product(2, 3, 4) << "\n"; // 24
}

Logical Fold Expressions

Logical folds (&&, ||, ,) with empty packs have well-defined behavior: && returns true, || returns false, and , returns void().

#include <iostream>
#include <type_traits>
#include <concepts>

// All true?
template <typename... Args>
constexpr bool allTrue(Args... args) {
    return (... && args);  // Empty pack → true
}

// Any true?
template <typename... Args>
constexpr bool anyTrue(Args... args) {
    return (... || args);  // Empty pack → false
}

// Compile-time type check all same
template <typename T, typename... Args>
constexpr bool allSame() {
    return (std::is_same_v<T, Args> && ...);
}

// Check if all types are integral
template <typename... Args>
constexpr bool allIntegral() {
    return (std::is_integral_v<Args> && ...);
}

int main() {
    std::cout << std::boolalpha;
    std::cout << "allTrue: " << allTrue(true, true, true) << "\n";      // true
    std::cout << "allTrue (false): " << allTrue(true, false) << "\n";   // false
    std::cout << "allTrue (empty): " << allTrue() << "\n";              // true

    std::cout << "anyTrue: " << anyTrue(false, true, false) << "\n";   // true
    std::cout << "anyTrue (all false): " << anyTrue(false, false) << "\n"; // false
    std::cout << "anyTrue (empty): " << anyTrue() << "\n";              // false

    std::cout << "allSame<int, int, int>: " << allSame<int, int, int>() << "\n";  // true
    std::cout << "allSame<int, double>: " << allSame<int, double>() << "\n";       // false

    std::cout << "allIntegral<int, long, char>: "
              << allIntegral<int, long, char>() << "\n";   // true
    std::cout << "allIntegral<int, double>: "
              << allIntegral<int, double>() << "\n";        // false
}

Comma Fold for Sequential Operations

The comma operator fold executes each expression in sequence.

#include <iostream>
#include <vector>

// Print all arguments (left fold with comma)
template <typename... Args>
void printAll(const Args&... args) {
    ((std::cout << args << " "), ...);  // Comma operator: (a, b, c) → executes each
    std::cout << "\n";
}

// Push all into a vector
template <typename T, typename... Args>
void pushAll(std::vector<T>& vec, Args&&... args) {
    (vec.push_back(std::forward<Args>(args)), ...);
}

// Call a function for each argument
template <typename Func, typename... Args>
void forEach(Func f, Args&&... args) {
    (f(std::forward<Args>(args)), ...);
}

int main() {
    printAll(1, 2, 3, "hello", 3.14);
    // 1 2 3 hello 3.14

    std::vector<int> v;
    pushAll(v, 1, 2, 3, 4, 5);
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";  // 1 2 3 4 5

    forEach([](auto x) { std::cout << x * 2 << " "; }, 1, 2, 3, 4);
    std::cout << "\n";  // 2 4 6 8
}

Fold with Custom Operators

Classes can override operators used in fold expressions.

#include <iostream>
#include <string>
#include <vector>

struct Data {
    std::vector<int> values;
};

Data& operator+=(Data& a, const Data& b) {
    a.values.insert(a.values.end(), b.values.begin(), b.values.end());
    return a;
}

Data operator+(Data a, const Data& b) {
    a += b;
    return a;
}

// Fold with custom += operator
template <typename... Args>
Data mergeData(Args&&... args) {
    Data result;
    (result += ... += std::forward<Args>(args));  // ((result += a) += b) += c
    return result;
}

int main() {
    Data d1{{1, 2}};
    Data d2{{3, 4}};
    Data d3{{5, 6}};

    Data merged = mergeData(d1, d2, d3);
    for (int v : merged.values) std::cout << v << " ";
    std::cout << "\n";  // 1 2 3 4 5 6
}

Fold in constexpr Context

Fold expressions in constexpr functions compile to compact code.

#include <iostream>
#include <array>

// Compile-time sum of parameter pack
template <int... Values>
struct CompileTimeSum {
    static constexpr int value = (Values + ...);
};

// Compile-time max (using ternary operator in fold)
template <typename... Args>
constexpr auto maxFold(const Args&... args) {
    static_assert(sizeof...(args) > 0, "max requires at least one argument");
    return ((args > ...) // This doesn't work for max
            // Use initializer list approach:
            std::max({args...}));
}

// Better compile-time max with fold
template <typename T, typename... Args>
constexpr T maxFold2(T first, Args... rest) {
    T result = first;
    ((result = (rest > result) ? rest : result), ...);
    return result;
}

int main() {
    std::cout << CompileTimeSum<1, 2, 3, 4, 5>::value << "\n";  // 15

    constexpr int m1 = maxFold2(10, 5, 20, 8, 15);
    std::cout << "max: " << m1 << "\n";  // 20

    constexpr int m2 = maxFold2(-5, -2, -10);
    std::cout << "max (negatives): " << m2 << "\n";  // -2
}

Common Mistakes

Mistake 1: Using wrong operator precedence in fold

// Fold has parsing issues with some operators
auto r = (args << ...);  // OK: ((a << b) << c)
auto r2 = ... << args;   // OK: (a << (b << c))

Know the precedence: << is left-to-right, so (args << ...) is left fold.

Mistake 2: Unary fold with empty pack and no identity

auto sum() { return (args + ...); }  // Error if Args is empty

Use binary fold (args + ... + 0) for empty-safe operations.

Mistake 3: Using fold with operator that has no identity

// No identity for / operator
auto divide(auto... args) { return (args / ...); }  // Fails for empty pack

Some operators have no identity. Use static_assert(sizeof...(args) > 0).

Mistake 4: Forgetting that comma fold evaluates left to right

((f(args), g(args)), ...);  // f(a), g(a), f(b), g(b), ...

Each element's expression is fully evaluated before the next.

Mistake 5: Using fold with assignment operators incorrectly

(result += ... += args);  // OK: binary left fold with +=

Assignment operators have right-to-left associativity, so the fold must match.

Practice Questions

  1. What is the output?
std::cout << (true && ... && std::array{true, false, true});

Answer: false — the fold expands to true && false && true = false.

  1. What does (args + ... + 0) do when args is empty? Answer: Returns 0 (the identity value). The binary fold provides a default for empty packs.

  2. Write a fold expression that checks if a value equals any of the arguments.

template <typename T, typename... Args>
bool equalsAny(const T& value, const Args&... args) {
    return ((value == args) || ...);
}
  1. What is the difference between (... + args) and (args + ...)? Answer: (... + args) is left fold (((a+b)+c)+d). (args + ...) is right fold (a+(b+(c+d))). For associative operators like +, they produce the same result.

  2. Can fold expressions work with the ternary ?: operator? Answer: No. The ternary operator is not a foldable operator. Use std::max({args...}) or expand manually.

FAQ

What are fold expressions in C++17

Fold expressions apply binary operators to parameter packs with minimal syntax. Example: (args + ...) expands to (a + (b + (c + ...))).

What is the difference between left fold and right fold

Left fold (... + args) groups from left: ((a+b)+c). Right fold (args + ...) groups from right: (a+(b+c)).

What is a binary fold expression

A binary fold provides an identity value: (args + ... + 0). This handles empty packs and is the most common fold form.

Can I use any operator in a fold expression

Yes, any of the 32 binary C++ operators can be used in fold expressions: +, -, *, /, %, ^, &, |, =, <, >, <<, >>, &&, ||, comma, etc.

How do I simulate if constexpr inside a fold

Use the ternary operator in the expansion: ((condition ? f(args) : g(args)), ...). Each element independently selects its branch.

Mini Project

Implement a CSV formatter using fold expressions that joins any number of values with a delimiter:

#include <iostream>
#include <string>
#include <sstream>

// Your csv format function using fold

int main() {
    std::cout << csv(", ", 1, 2, 3) << "\n";           // 1, 2, 3
    std::cout << csv(", ", "apple", "banana") << "\n";  // apple, banana
    std::cout << csv(", ", 3.14, "text", 42) << "\n";   // 3.14, text, 42
    std::cout << csv(", ") << "\n";                     // (empty string)

    // Different delimiter
    std::cout << csv("|", 10, 20, 30) << "\n";          // 10|20|30
}

This project demonstrates how C++ fold expressions create concise, type-safe variadic functions. Compare with Python's str.join() which only works with strings — the C++ version is generic across all types.

What's Next

You now master fold expressions — the most concise tool for variadic operations. Next, you will learn coroutines (C++20), which enable cooperative multitasking with suspend/resume semantics.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro