Fold expressions
A fold expression applies an operator to every element of a pack and produces one value. Where lesson 040 needed a base case and a recursive call, a fold writes the same reduction in one line.
The syntax places the pack next to the operator with three dots: (args + ...) is a unary right fold, (0 + ... + args) a binary fold with an initial value, and (std::cout << ... << args) a fold over the stream operator. This lesson writes each form and one that fails on an empty pack.
A unary fold
(args + ...) expands to a + (b + (c + ...)) for the pack args. The operator is applied between the elements, and the result is a single value of whatever type the additions produce.
The program sums four ints and then three doubles with the same function template. Each call instantiates the fold for its own types, and the printed values are the sums.
A unary fold needs at least one element, because there is no initial value to stand in for an empty pack. The binary form in the next section removes that restriction.
#include <iostream>
template <typename... Ts>
auto sum(Ts... values)
{
return (values + ...);
}
int main()
{
std::cout << sum(1, 2, 3, 4) << '\n';
std::cout << sum(1.5, 2.5, 3.0) << '\n';
}
g++ -std=c++23 -Wall -Wextra unary.cpp -o unary
./unary
10
7
A binary fold with an initial value
(0 + ... + values) starts from zero and folds the pack into it. The initial value fixes the type of the result and defines what an empty pack returns, so the function works for zero arguments as well.
The program calls the sum with three ints and then with no arguments at all. The empty call returns 0, which the unary version could not do.
The initial value also decides the arithmetic: a 0.0 initial value makes the whole fold floating point even when every element is an int. That is the same rule as an ordinary expression, applied to the first operand.
#include <iostream>
template <typename... Ts>
auto sum(Ts... values)
{
return (0 + ... + values);
}
int main()
{
std::cout << sum(1, 2, 3) << '\n';
std::cout << sum() << '\n';
}
g++ -std=c++23 -Wall -Wextra binary.cpp -o binary
./binary
6
0
Logical folds
The same syntax works with && and ||. (flags && ...) is true when every element is true, and (flags || ...) is true when at least one is, with short-circuit evaluation preserved left to right.
The program creates four bool variables and folds them twice. The printed pair shows all-true and any-true, and the functions are usable in a condition without a loop.
An empty && fold is true and an empty || fold is false, which are the identity values of the two operators. That is why the logical folds are often written without an initial value.
#include <iostream>
template <typename... Ts>
bool all(Ts... values)
{
return (values && ...);
}
template <typename... Ts>
bool any(Ts... values)
{
return (values || ...);
}
int main()
{
bool a = true;
bool b = true;
bool c = false;
std::cout << all(a, b) << ' ' << any(a, c) << '\n';
}
g++ -std=c++23 -Wall -Wextra logical.cpp -o logical
./logical
1 1
Folding over the comma operator
A comma fold evaluates each element for its side effect and returns the last value. (std::cout << ... << values) prints every element in order, which is the variadic print of the previous lesson written as one expression.
The program prints an int, a double and a string literal without a recursive helper. The fold expands to a chain of << calls, exactly as if they had been written by hand.
A comma fold with an initial value is the usual way to apply a function to every element: (f(values), ...). The order of evaluation is left to right and the result is the value of the last call.
#include <iostream>
template <typename... Ts>
void print(const Ts&... values)
{
(std::cout << ... << values) << '\n';
}
int main()
{
print(1, 2.5, "three");
}
g++ -std=c++23 -Wall -Wextra comma.cpp -o comma
./comma
12.5three
An empty pack in a unary fold
A unary fold with + has no value to start from, so an empty pack cannot be reduced. The program calls the unary sum with no arguments and the compiler reports that the fold has nothing to apply the operator to.
The message names the function and the fold expression, and the fix is the binary form with an initial value, or a base case as in the previous lesson.
&& and || are the exceptions: their identity values let an empty unary fold compile and return true or false. Every other operator needs the binary form when the pack may be empty.
template <typename... Ts>
auto sum(Ts... values)
{
return (values + ...);
}
int main()
{
return sum();
}
g++ -std=c++23 -Wall -Wextra empty.cpp -o empty
empty.cpp: In instantiation of 'auto sum(Ts ...) [with Ts = {}]':
empty.cpp:9:15: required from here
9 | return sum();
| ~~~^~
empty.cpp:4:20: error: fold of empty expansion over operator+
4 | return (values + ...);
| ~~~~~~~~^~~~~~
Exercises
- Write a fold that returns the product of a pack and test it with one, three and zero arguments.
- Rewrite the recursive sum of lesson 040 as a binary fold and compare the two functions.
- Write a print() that separates its arguments with commas using a fold over comma.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a fold expression?
Answer
An operator applied across a parameter pack to produce one value
The pack is expanded with the operator between its elements, which replaces the recursion of the previous lesson.
-
What does
(args + ...)mean?Answer
A unary right fold that adds every element
The operator is applied between the elements from the right, and the result is their sum.
-
Why use
(0 + ... + args)instead of(args + ...)?Answer
The initial value gives the empty pack a result
The identity element makes the fold well-defined when the pack has no arguments.
-
What is a comma fold used for?
Answer
Applying a side effect to every element in order
The comma operator discards the values and keeps the effects, which is how a print-all loop is written.
-
What does this program print?
#include <iostream> template <typename... Ts> auto sum(Ts... values) { return (values + ...); } int main() { std::cout << sum(1, 2, 3, 4) << ' ' << sum(3, 4); }Answer
10 7
The unary right fold adds each pack, and the two calls give 10 and 7.
-
What does this program print?
#include <iostream> template <typename... Ts> auto sum(Ts... values) { return (0 + ... + values); } int main() { std::cout << sum(1, 2, 3) << ' ' << sum(); }Answer
6 0
The binary fold starts at zero, so the empty pack gives zero and the three values give six.
-
What does this program print?
#include <iostream> template <typename... Ts> bool all(Ts... values) { return (values && ...); } template <typename... Ts> bool any(Ts... values) { return (values || ...); } int main() { std::cout << all(true, true, true) << ' ' << any(false, true, false); }Answer
1 1
The conjunction of three trues is true, and the disjunction with one true is true; true prints as 1.
-
What does this program print?
#include <iostream> template <typename... Ts> void show(Ts... values) { ((std::cout << values), ...); } int main() { show(12.5, "three"); }Answer
12.5three
The comma fold prints each element in order and inserts nothing between them.
-
What does folding
+over an empty pack without an initial value report?Answer
fold of empty expansion over operator+
The unary fold has no value for an empty pack, and the compiler names the operator.
-
Which fold would you write to count the arguments?
Answer
(0 + ... + (args, 1))The comma inside the fold turns each element into one, and the outer addition sums them.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev