Binary Left Folds with Initial Values in C++

This article explains how binary left fold expressions function in modern C++, specifically detailing how they combine an explicit initial accumulator value with an unexpanded parameter pack. Introduced in C++17, fold expressions simplify variadic templates by replacing recursive template instantiations with concise operator syntax. Readers will learn the exact syntax, expansion mechanics, evaluation order, and behavior of binary left folds, illustrated with practical examples that highlight their left-associative nature.

Syntax and Expansion Pattern

A binary left fold uses an explicit initial value alongside a parameter pack and a supported binary operator. The standard syntax is defined as:

(init op ... op pack)

In this syntax:

  • init represents the starting value (the accumulator).
  • op represents any valid C++ fold operator (such as +, -, *, <<, etc.). Both instances of op must be the identical operator token.
  • pack represents the variadic parameter pack.

When the compiler expands this expression for a parameter pack containing elements E1, E2, ..., En, it structures the operations with strict left associativity:

(((init op E1) op E2) op ...) op En

The evaluation begins by applying the operator to init and the first element E1. The result of that operation then serves as the left-hand operand for the next operation with E2, continuing sequentially until the pack is exhausted.

Step-by-Step Evaluation Order

Because evaluation is left-associative, grouping happens strictly from left to right. This behavior is crucial when using non-associative operators such as subtraction (-) or division (/).

Consider a subtraction fold function:

template <typename Init, typename... Args>
auto subtract_from(Init init, Args... args) {
    return (init - ... - args);
}

If invoked with subtract_from(100, 10, 5, 2), the compiler expands the expression into:

(((100 - 10) - 5) - 2)

The runtime evaluation proceeds as:

  1. (100 - 10) evaluates to 90.
  2. (90 - 5) evaluates to 85.
  3. (85 - 2) evaluates to 78.

If this had been a right fold ((args - ... - init)), the grouping would have been (10 - (5 - (2 - 100))), yielding a completely different result.

Handling Empty Parameter Packs

One significant advantage of a binary fold over a unary fold is its behavior when the parameter pack is empty.

Unary folds (such as (... + pack)) require the pack to have at least one element for most operators, failing to compile if the pack is empty. In contrast, a binary left fold is always well-formed even if the parameter pack contains zero arguments:

subtract_from(100); // Parameter pack 'args' is empty

When pack is empty, (init op ... op pack) simply evaluates to the init expression directly, returning 100 without performing any operations.

Practical Use Cases

Binary left folds are frequently used to chain operations onto an existing object or accumulator, such as writing multiple values into an output stream:

template <typename... Args>
void print_all(std::ostream& os, const Args&... args) {
    (os << ... << args);
}

Here, os serves as the initial accumulator. The left-associative expansion ensures items are streamed in the exact order they were passed: ((os << arg1) << arg2) << arg3.

They are equally useful for pushing elements into containers:

template <typename T, typename... Args>
void append_to_vector(std::vector<T>& vec, Args&&... args) {
    (vec.push_back(std::forward<Args>(args)), ...);
}

Through left-associative chaining, binary left folds guarantee deterministic, ordered processing of parameter packs starting from an established base state.