C++ / TEMPLATES AND GENERIC PROGRAMMING
Fold expressions over parameter packs
Collapse a parameter pack into a single expression with C++17 fold expressions, choosing direction and an identity value so empty packs still compile.
What you will learn
- Read the position of ... to tell a left fold from a right fold
- Use the binary form (init op ... op pack) so an empty pack still compiles
- Drive per-element side effects with a comma fold in guaranteed order
- Recall that only &&, || and , have a value for an empty unary fold
Understanding Fold expressions over parameter packs
A fold expression is a rewrite rule, not a function call. When the compiler sees (... + vs) it expands the pack once and inserts a + between every element, producing a single expression such as ((1 + 2) + 3) + 4. That is why a fold costs less to compile than the head/tail overload pair you would otherwise write: there is no second overload, no base case, and no N template instantiations. Any of the 32 binary operators can be the glue, including the comma operator, which is what turns a fold into a per-element loop.
Where you put the ... decides how the resulting chain is parenthesised. With ... before the pack you get a left fold and the first element ends up innermost, so (... - vs) becomes ((v1 - v2) - v3); with ... after the pack you get a right fold that nests from the far end, so (vs - ...) becomes v1 - (v2 - v3). The two agree for associative operators like + on int and disagree silently for -, /, << and for anything whose intermediate type changes along the way. Default to a left fold, because the operators you usually fold, stream insertion above all, were designed to chain leftwards.
The binary forms, (init op ... op pack) and (pack op ... op init), add one operand outside the pack that becomes the seed at the innermost end of the chain, while ... still fixes the direction. Their real job is empty packs: a unary fold over zero elements is well-formed only for &&, || and , (giving true, false and void()), so (... + vs) called with no arguments is a hard error while (0 + ... + vs) is 0. Because the expansion is genuine operator syntax rather than a call, a && fold still short-circuits and a , fold is still sequenced left to right, and those guarantees vanish the moment you fold an overloaded &&, || or , on a class type, since an overload is a function call.
placeholder
<iostream>
template <typename... Ts>
auto sum_left(Ts... vs) { return (... + vs); } // ((v1+v2)+v3)+v4
template <typename... Ts>
auto sub_left(Ts... vs) { return (... - vs); } // ((v1-v2)-v3)-v4
template <typename... Ts>
auto sub_right(Ts... vs) { return (vs - ...); } // v1-(v2-(v3-v4))
template <typename... Ts>
auto sum_or_zero(Ts... vs) { return (0 + ... + vs); } // legal with no args
template <typename... Ts>
void write(const Ts&... vs) { (std::cout << ... << vs) << '\n'; }
int main() {
std::cout << sum_left(1, 2, 3, 4) << '\n';
std::cout << sub_left(1, 2, 3, 4) << '\n';
std::cout << sub_right(1, 2, 3, 4) << '\n';
std::cout << sum_or_zero() << '\n';
write("count=", 4, " avg=", 2.5);
}
A fold expression is a compile-time rewrite that drops one binary operator between every element of a pack, and the side the ... sits on decides how that chain is parenthesised.
Worked examples
Short-circuiting && and || folds
Shows that a boolean fold expands to a real operator chain, so later elements are never evaluated once the answer is known.
<iostream>
bool check(int n) {
std::cout << "check(" << n << ")\n";
return n > 0;
}
template <typename... Ts>
bool all_positive(Ts... vs) { return (... && check(vs)); }
template <typename... Ts>
bool any_positive(Ts... vs) { return (... || check(vs)); }
int main() {
std::cout << all_positive(1, -2, 3) << '\n';
std::cout << any_positive() << '\n';
}
Example explained
Line 1(... && check(vs)) expands to (check(1) && check(-2)) && check(3), an ordinary && chain.
Line 2check(-2) returns false, so the built-in && skips check(3) and only two check lines appear.
Line 3check(vs) needs no extra parentheses because a function call is already a valid fold pattern.
Line 4any_positive() folds an empty pack with ||, which is defined as false and streams as 0.
Comma folds for per-element work
Uses a comma fold to run the same statement on every element with guaranteed left-to-right sequencing.
<iostream>
template <typename... Ts>
void numbered(const Ts&... vs) {
int i = 0;
((std::cout << ++i << ": " << vs << '\n'), ...);
}
int main() {
numbered("alpha", 7, 1.5);
}
Example explained
Line 1The outer parentheses belong to the fold; the inner pair is required because the pattern contains <<.
Line 2The comma operator sequences each element fully before the next, so ++i yields 1, then 2, then 3.
Line 3The fold's own value is discarded here; the point of the expression is its side effects.
Line 4Each element picks its own operator<< during expansion, so mixed argument types need no extra work.
Folding at the type level
Folds a pack of compile-time bool constants to answer whether a type appears in a type list.
<iostream>
<type_traits>
template <typename T, typename... Ts>
constexpr bool is_one_of = (std::is_same_v<T, Ts> || ...);
int main() {
std::cout << is_one_of<int, char, int, double> << '\n';
std::cout << is_one_of<float, char, int, double> << '\n';
std::cout << is_one_of<int> << '\n';
}
Example explained
Line 1The pattern std::is_same_v<T, Ts> mentions the pack, so the fold produces one bool per element.
Line 2is_one_of<int, char, int, double> expands to false || (true || false), a constant expression that is true.
Line 3is_one_of<int> leaves Ts empty, and the empty || fold is false, so an unmatched type answers 0 instead of failing to compile.
Important notes
The grammar allows only a cast-expression as the fold pattern, so ((std::cout << v << ' '), ...) compiles while (std::cout << v << ' ', ...) does not.
Folding an overloaded &&, || or , on a class type calls a function, which loses short-circuiting and left-to-right sequencing; that is one more reason not to overload those operators.
Common mistakes
Dropping the enclosing parentheses, as in return ... + vs;, since they are part of the fold grammar and not decoration; the compiler answers with a parse error pointing near the ... rather than at the real problem.
Treating (vs - ...) and (... - vs) as interchangeable: for the arguments (10, 3, 2) the first gives 9 and the second gives 5, and no diagnostic ever appears.
Writing (... + vs) in a helper a caller may invoke with no arguments; every non-empty call compiles, and the error only surfaces at the instantiation that passes nothing, so use (0 + ... + vs) instead.
Try it yourself
Change, predict, then run
Write join_bits(...) that ORs all its arguments together using 0 as the seed, and all_even(...) that folds && over the pattern (vs % 2 == 0). Print both for the arguments (1, 2, 4) and again with no arguments at all, and note which value the empty && fold produces.
Open the C++ workspaceCheck your understanding
Given template <typename... Ts> auto f(Ts... vs) { return (vs - ...); }, what does f(10, 3, 2) return?
- 5
- 9
- -9
- It does not compile, because subtraction has no identity element
Show answer
The ... sits to the right of the pack, so this is a unary right fold that nests from the last element: 10 - (3 - 2), which is 9. The answer 5 is the left-fold result ((10 - 3) - 2) that (... - vs) would give. No identity is needed here because the pack is not empty; identities only matter when zero elements are folded.