5.2.16 · D5C++ Programming

Question bank — Variadic templates — parameter packs, fold expressions

2,247 words10 min readBack to topic

For deeper background see the parent: Variadic templates (parent).


0. Naming the symbols first (read before the traps)

Every item below reuses four short names. They are just conventional letters, not keywords — you could rename them. Here is exactly what each stands for, with a concrete call so nothing is a mystery symbol.

Figure — Variadic templates — parameter packs, fold expressions

1. Two pictures you must hold in your head

The whole topic lives in one distinction: expanding a pack (laying its elements in a row) vs folding it (collapsing the row with an operator, nested to one side). Every trap in this bank is really a question about one of these two shapes.

Figure — Variadic templates — parameter packs, fold expressions
Figure — Variadic templates — parameter packs, fold expressions

True or false — justify

A parameter pack can hold zero elements.
True. A pack means "zero or more" — f() with an empty pack is legal, which is exactly why recursive expansion needs a base-case overload to catch it.
sizeof...(Ts) is evaluated at run time.
False. It is a constexpr size_t known at compile time; you can use it in if constexpr, array sizes, and other compile-time contexts.
(xs + ...) and (... + xs) always produce the same numeric result.
True for + because it's associative, but the association order still differs (right-nested vs left-nested); for non-associative operators like - or / the results diverge.
The parentheses in (xs + ...) are optional stylistic choices.
False. They are a mandatory part of fold syntax — the grammar rule is ( pack op ... ), so removing them yields a parse error, not a warning.
A unary fold with * over an empty pack yields 1.
False. Only &&, ||, and , have defined empty-pack identities; * over an empty pack is a compile error, not an implicit 1.
typename... Ts and Ts... args both declare packs.
True. The first declares a type pack, the second a value (function-argument) pack; ... before the name always means "declare a pack."
f(args)... is the same as f(args...).
False. f(args)... expands to f(a0), f(a1), f(a2) (call f on each), while f(args...) is a single call f(a0, a1, a2).
A variadic function template can also take normal (non-pack) parameters before the pack.
True. e.g. void log(const char* tag, Ts... rest). The pack simply absorbs everything after the fixed parameters.
Fold expressions completely replace recursive pack processing.
False. Folds handle "combine with one binary operator" cases; when each element needs different logic based on type or position, recursion (or if constexpr) is still the tool.
(cout << xs << ' ', ...) guarantees left-to-right printing order.
True. The comma operator in a fold sequences evaluations left→right, so side effects (printing) happen in argument order.

Spot the error

return xs + ...; — what's wrong, and what does the compiler say?
Missing the mandatory fold parentheses; write return (xs + ...);. Without them GCC/Clang report roughly error: expected primary-expression before '...' token because xs + ... isn't a recognised fold.
bool ok = (bs && ...); when bs might be empty — is this safe?
Safe. && is one of the three operators with an empty-pack identity (true), so an empty pack returns true instead of erroring.
auto s = (xs * ...); where xs could be empty — what does the diagnostic look like?
Compile error on empty pack: error: fold of empty expansion over operator* (Clang) — * has no fold identity. Fix with a binary fold and an init: (1 * ... * xs).
void f(Ts... args) { g(args); } — why won't this compile?
args is a pack, not a single value; the diagnostic is error: parameter packs not expanded with '...'. You must expand it: g(args...). Bare args is illegal except inside sizeof....
size_t n = sizeof(Ts); to count pack elements — error?
Yes: error: parameter packs not expanded with '...'. sizeof(Ts) on a pack is ill-formed; the count operator is sizeof...(Ts). The ... changes the meaning entirely.
template<typename... Ts>
void print(Ts... rest) { std::cout << ...; }   // ill-formed
Why is this broken?
std::cout << ... has no pack expression on the fold's operand side, so the compiler emits something like error: expected expression before '...'. You need ((std::cout << rest << ' '), ...); (a comma fold) or a recursive head/tail split.
template<typename T, typename... Rest>
void print(T first, Rest... rest) { std::cout << first; print(rest...); }
// no  void print() {}  overload
What happens when the pack empties?
The final print() call finds no matching overload; the compiler fails with error: no matching function for call to 'print()' — recursion has no terminator.
auto avg(Ts... xs) { return (xs + ...) / sizeof...(xs); } — subtle bug?
On an empty pack (xs + ...) fails to compile (fold of empty expansion over operator+); even if it compiled, dividing by sizeof... = 0 is undefined. sizeof...(xs) being unsigned can also skew a signed average.

Why questions

Why do variadic templates exist instead of just overloading for 1..N args by hand?
Hand-overloading is repetitive and has an arbitrary cap; a pack folds "1 to N arguments of any types" into one reusable, type-safe pattern the compiler instantiates on demand.
Why is ... placed after a pattern for expansion but before the name for declaration?
It's a positional convention: leading ... creates a pack, trailing ... repeats the pattern to its left once per pack element (f(args)...f(a0), f(a1), ...).
Why does the fold direction (left vs right) matter for - but not +?
- is non-associative, so 1-(2-3)=2 differs from (1-2)-3=-4; + is associative so all groupings give the same value — only the nesting shape changes.
Why prefer a comma fold over recursion for print_each?
The comma fold is one line, needs no base case, and guarantees left-to-right side effects — recursion here would be two functions for the same idea.
Why is sizeof... a compile-time constant while a recursive count would be run-time?
The pack's size is fixed the instant the template is instantiated, so the compiler substitutes the literal count directly into the code — nothing is computed while the program runs.
How does a fold pair with Perfect Forwarding — what does forwarding actually preserve?
In f(std::forward<Ts>(args)...) the ... repeats std::forward<Ts>(arg) once per element. Each std::forward<Ts> casts back to Ts&&, so an argument that arrived as an rvalue stays an rvalue (movable) and an lvalue stays an lvalue — the pack expansion applies that value-category-preserving cast to every element uniformly.
Why does std::tuple construction fall out of pack expansion so naturally?
A tuple is an ordered heterogeneous list, and std::make_tuple(args...) expands the pack into exactly that comma-separated argument list — the pack's "one type/value per slot" structure is the same shape as a tuple's fields, so no glue code is needed.
Why does if constexpr often replace recursion when processing packs by type?
It lets one function branch at compile time on sizeof... or type traits, and discards the untaken branch, so you avoid writing separate base/recursive overloads to terminate.

Edge cases

What does a unary right fold of a single-element pack (x + ...) reduce to?
Just x — with one element there is nothing to combine, so the fold returns that element unchanged.
What does a unary left fold of a single-element pack (... + x) reduce to?
Also just x. Left and right folds are only distinguishable with 2+ elements; on a single element both collapse to the lone value, so (... + x) and (x + ...) are identical here.
What does (... , args) (comma fold) over an empty pack evaluate to?
void() — the comma is one of the three operators with a defined empty-pack identity, so it's a valid no-op.
Can a template have two parameter packs in the same parameter list?
Generally no for function templates — only the last parameter can be a pack, since the compiler couldn't split arguments between two greedy packs (partial specializations are a narrow exception).
What happens if you call a variadic template with zero arguments?
It's valid; sizeof... is 0, unary folds must use one of the three empty-safe operators or switch to a binary init, and recursive versions immediately hit the base case.
Does (0 + ... + xs) change the result when xs is non-empty?
No meaningful change for +: it just seeds the left-fold with 0, giving ((0+a0)+a1)+...; the safety only matters when xs might be empty.
For a binary right fold (xs + ... + init), where does init sit in the nesting?
At the deepest right position: a0 + (a1 + (... + (an + init)))init is combined last from the right, guaranteeing a value even for an empty pack.
Is (xs op ...) legal when the elements have mixed types?
Yes, as long as op is defined between the successive operand types (via built-ins or Operator Overloading); the result type follows the usual promotion/overload-resolution rules.

Recall Interactive self-test (hints included — check yourself)

Answer each aloud, then reveal the hint by expanding your memory of the sections above:

  1. Name the three operators whose unary fold is legal over an empty pack, and give each identity. Hint: they are the short-circuit boolean pair plus the sequencing operator → &&true, ||false, ,void().
  2. State why the parentheses in a fold are mandatory in one sentence. Hint: think grammar production, not style → ( pack op ... ) is the rule, so removing them = parse error.
  3. Explain why (xs - ...)(... - xs). Hint: draw the nesting → right = 1-(2-3)=2, left = (1-2)-3=-4; - is non-associative.

If any of your answers was a bare fact with no reason attached, that item is the one to revisit first.