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 of op must 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:

  1. c + z is evaluated first.
  2. The result of (c + z) is then combined with b.
  3. Finally, a is 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 0

Concrete 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:

  1. Short-Circuit Operators: When using && or ||, short-circuiting applies according to the grouped expression. In a || (b || (c || init)), if a evaluates to true, the nested right-hand side (b || (c || init)) is not evaluated.
  2. Comma Operator: In (pack , ... , init), the expansion E1 , (E2 , (... , (EN , init))) evaluates each operand from left to right due to the sequenced-before rules of the comma operator, ultimately returning the value of init.
  3. Assignment Operators: Because assignment operators (such as =, +=, etc.) are naturally right-associative in C++, binary right folds mirror native assignment semantics when chaining operations.