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:
initrepresents the starting value (the accumulator).oprepresents any valid C++ fold operator (such as+,-,*,<<, etc.). Both instances ofopmust be the identical operator token.packrepresents 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 EnThe 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:
(100 - 10)evaluates to90.(90 - 5)evaluates to85.(85 - 2)evaluates to78.
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 emptyWhen 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.