C++ Binary Right Fold Expansion Semantics
Fold expressions, introduced in C++17, provide a concise mechanism for unpacking variadic parameter packs over binary operators. A binary right fold expands an unexpanded parameter pack and an explicit initial value from right to left, creating a right-associative sequence of operations. This article covers the syntax, formal expansion rules, evaluation characteristics, and practical behavior of binary right folds in modern C++.
Syntax and Structure
A binary right fold requires an unexpanded parameter pack, an explicit initial operand, and two identical binary operators surrounding an ellipsis. The standard syntax is defined as:
( pack op ... op init )pack: An expression containing an unexpanded parameter pack.op: Any valid C++ fold-supported binary operator (both instances ofopmust be identical)....: The literal ellipsis token indicating pack expansion.init: An expression representing the initial value or terminal operand, which does not contain an unexpanded pack.- Parentheses
(...): The outer parentheses are mandatory; omitting them results in a compiler error.
Expansion Semantics
When the compiler instantiates a binary right fold, it resolves the
parameter pack into individual elements \(E_1,
E_2, \dots, E_N\). The expansion nests operations starting from
the rightmost elements, pairing the final pack element with the
init expression first.
Mathematically, the expansion of (pack op ... op init)
yields:
\[E_1 \text{ op } (E_2 \text{ op } (\dots \text{ op } (E_{N-1} \text{ op } (E_N \text{ op } init))\dots))\]
Non-Empty Pack Behavior
If the pack expands to three elements a, b, c and the
initial value is z, the expression
(pack + ... + z) expands directly to:
a + (b + (c + z))Because of the right-nested parentheses, evaluation of the operations proceeds from the inside out:
c + zis evaluated first.- The result of
(c + z)is then combined withb. - Finally,
ais combined with the result of(b + (c + z)).
Empty Pack Behavior
Unlike unary folds, which are invalid for empty packs on most
operators (with exceptions for &&, ||,
and ,), a binary right fold is well-formed when the
parameter pack is empty. If the pack contains zero elements, the entire
expression evaluates strictly to the init expression:
template <typename... Args>
auto foldExample(Args... args) {
return (args + ... + 0);
}
// When args is empty:
foldExample(); // Evaluates directly to 0Concrete Example: Left Fold vs. Right Fold
The distinction in expansion direction is particularly critical when
using non-associative operators, such as subtraction (-) or
division (/).
#include <iostream>
template <typename... Args>
auto binaryRightFold(Args... args) {
return (args - ... - 1);
}
template <typename... Args>
auto binaryLeftFold(Args... args) {
return (1 - ... - args);
}
int main() {
// Values: 10, 5, 2
// Right fold expands to: 10 - (5 - (2 - 1))
// 2 - 1 = 1
// 5 - 1 = 4
// 10 - 4 = 6
std::cout << binaryRightFold(10, 5, 2) << '\n'; // Outputs 6
// Left fold expands to: ((1 - 10) - 5) - 2
// 1 - 10 = -9
// -9 - 5 = -14
// -14 - 2 = -16
std::cout << binaryLeftFold(10, 5, 2) << '\n'; // Outputs -16
}Operator Precedence and Evaluation Order
Even though right folds associate from right to left, C++ sequencing rules still apply to subexpressions:
- Short-Circuit Operators: When using
&&or||, short-circuiting applies according to the grouped expression. Ina || (b || (c || init)), ifaevaluates totrue, the nested right-hand side(b || (c || init))is not evaluated. - Comma Operator: In
(pack , ... , init), the expansionE1 , (E2 , (... , (EN , init)))evaluates each operand from left to right due to the sequenced-before rules of the comma operator, ultimately returning the value ofinit. - Assignment Operators: Because assignment operators
(such as
=,+=, etc.) are naturally right-associative in C++, binary right folds mirror native assignment semantics when chaining operations.