Skip to content
jieqi's archive
Go back

C++: Parameter Packs and Fold Expressions

Contents
template <class... Ts> void f(Ts... xs) { g(xs...); }
                ^^^^^^        ^^^^^^^^      ^^^^^
                type pack     function      expansion
                              param pack

... does three different jobs there, with different rules for each: declaring a pack, expanding one, and folding one.

Parameter Packs

A named placeholder for zero or more entities, declared with .... It isn’t a type and it isn’t a container. Nothing about it survives to runtime, since it’s purely a compile-time substitution.

template <class... Ts>   // Ts: a type pack
void f(Ts... xs);        // xs: a function parameter pack

Two packs here, and they always have the same length, element for element. That’s what makes std::forward<Ts>(xs)... work.

A “variadic template” is just a template that declares one. There’s no separate feature.

sizeof...(Ts) gives the length. It’s the only thing you can do to a pack without expanding it.

Expansion

There’s nothing you can do with a pack except expand it. Write pattern..., and the compiler stamps out one copy of the pattern per element, separated by commas.

Assume this call throughout:

template <class... Ts>
void demo(Ts... xs);

demo(1, 2, 3);   // xs = 1, 2, 3
xs           // illegal on its own
xs...        // 1, 2, 3
h(xs)...     // h(1), h(2), h(3)
h(xs...)     // h(1, 2, 3)

The pattern is everything to the left of the ... that contains the pack name. The last two lines differ only in where the dots sit, and they compile to completely different calls.

Two packs in one pattern expand in lockstep and must be the same length. Call demo(1, 2.5, 'c') so Ts is int, double, char:

std::forward<Ts>(xs)...
// std::forward<int>(1), std::forward<double>(2.5), std::forward<char>('c')

Expansion produces a comma separated list, so it only compiles where the grammar already accepts one:

f(xs...)                // call arguments
T obj{xs...}            // init list
Tmpl<Ts...>             // template argument list
struct D : Bs... { };   // base specifiers
[xs...] { };            // lambda capture

A statement sequence isn’t a comma list, and neither is 1 - 2 - 3. Those are the cases folds were added for.

Fold Expressions

C++17 added folds to cover them, joining the elements with a binary operator instead of commas. There are four forms, and subtraction tells them apart, since it isn’t associative and each one gives a different answer:

(xs - ...)         unary right    1 - (2 - 3)           1 - (-1)     =    2
(... - xs)         unary left     (1 - 2) - 3           (-1) - 3     =   -4
(xs - ... - 100)   binary right   1 - (2 - (3 - 100))   1 - 99       =  -98
(100 - ... - xs)   binary left    ((100 - 1) - 2) - 3   99 - 2 - 3   =   94

The ... sits on the side where the nesting is deepest, so dots to the right of the pack means right fold. In the binary forms the init value ends up as the outermost operand, on the same side as the dots. The parentheses around the whole fold are mandatory.

Most folds use an associative operator, where none of that matters:

template <class... Ts>
auto sum(Ts... xs) { return (xs + ...); }

template <class... Ts>
void print(Ts... xs) { ((std::cout << xs << ' '), ...); }   // comma fold

The comma fold covers the statement case: do this to each element, left to right, with the sequencing guaranteed. Before C++17 you got the same effect out of an array-init trick, which existed only to put the expansion somewhere a comma list was legal:

int dummy[] = { (f(xs), 0)... };

Empty Packs

A unary fold over zero elements is ill-formed, except && (yields true), || (false) and , (void()). That’s why the binary forms take an init value:

(xs + ...)       // zero args: error, no identity for +
(0 + ... + xs)   // zero args: 0
error: fold of empty expansion over operator+

How They Compose

variadic template
└─ declares → parameter pack ───── sizeof...(pack)   count, no expansion
                    │
                    └─ used via → expansion
                                   ├─ pattern...        joined by commas
                                   └─ (pattern op ...)  joined by an operator

Folds go through the same substitution as any other expansion. What changes is only what gets put between the copies.

Check

These two differ only in the position of the .... What does each call?

template <class... Ts> void a(Ts... xs) { g(h(xs)...); }
template <class... Ts> void b(Ts... xs) { g(h(xs...)); }
Answer
a(1, 2, 3);   // g(h(1), h(2), h(3))   three calls to h, one to g
b(1, 2, 3);   // g(h(1, 2, 3))         one call to h, one to g

In a the pattern is h(xs), so the whole call to h is duplicated per element. In b the pattern is just xs, expanding inside h’s argument list, so h is called once with all three.

a needs an h taking one argument and a g taking three. b needs the reverse.

Previous Post
C++: Polymorphism - CRTP
Next Post
C++: Concurrency Pt 2 - Sharing