Exercises — Variadic templates — parameter packs, fold expressions
Before we start, one picture pins down the vocabulary we reuse in every problem.

Level 1 — Recognition
L1.1 — Read the ellipsis
State, for each line, whether ... declares a pack or expands one:
template<typename... Ts>void f(Ts... args)g(args...)(args + ...)
Recall Solution
Our reading key: ... before the name declares; ... after a pattern expands.
typename... Ts—...is beforeTs→ declare a type pack.Ts... args—...is beforeargs→ declare a value pack.args...—...follows the patternargs→ expand (producesa0, a1, a2, ...).(args + ...)—...follows the fold pattern → expand as a fold.
So: declare, declare, expand, expand.
L1.2 — Count vs size
For f(1, 'a', 3.14, "hi"), what does sizeof...(Ts) return, and why is it not sizeof(Ts)?
Recall Solution
There are 4 arguments, so sizeof...(Ts) . It counts how many template parameters
are in the pack — a constexpr size_t known at compile time.
sizeof(Ts) is illegal: Ts is a whole pile of types, not one type, so "how many bytes is it"
has no answer. The ... changes the question from bytes to count.
Level 2 — Application
L2.1 — Evaluate a right fold
Given sum(Ts... xs){ return (xs + ...); }, compute sum(10, 20, 30, 40) and write the nesting.
Recall Solution
Unary right fold nests from the right:
Answer: 100. (Since + is associative the value would match a left fold, but the shape
is right-nested.)
L2.2 — Evaluate a left fold with a non-associative op
Compute sub(1, 2, 3, 4) where sub uses the left fold (... - xs).
Recall Solution
Unary left fold nests from the left (leftmost op binds first): Answer: -8.
L2.3 — Comma fold for side effects
What does ((std::cout << xs << ' '), ...); print for print_each('C', '+', '+')?
Recall Solution
The comma fold evaluates its pattern once per element, left to right, discarding each result.
So it runs cout << 'C' << ' ', then cout << '+' << ' ', then cout << '+' << ' '.
Output (note trailing space): C + + .
We use the comma operator precisely because we care about the side effect (printing) in order,
not about combining return values.
Level 3 — Analysis
L3.1 — The empty pack
Which of these compile when the pack xs is empty, and what value results?
(xs + ...)(xs && ...)(xs || ...)(xs, ...)(0 + ... + xs)
Recall Solution
Only three unary folds have a defined identity for an empty pack; the rest are compile errors.
(xs + ...)— compile error (no identity element for+in a unary fold).(xs && ...)— compiles, valuetrue.(xs || ...)— compiles, valuefalse.(xs, ...)— compiles, valuevoid()(nothing).(0 + ... + xs)— compiles, value0. This is a binary fold whoseinit = 0supplies the missing identity, so it is safe for any size, including zero.
Lesson: to safely sum a possibly-empty pack, always give an init: (0 + ... + xs).
L3.2 — Predict the association shape
For concat that string-joins with +, does (xs + ...) vs (... + xs) change the result?
Given xs = "a","b","c" (as std::string), give both outputs.
Recall Solution
String + is associative for the produced characters, so both give "abc".
- Right fold
(xs + ...):"a" + ("b" + "c")="a" + "bc"="abc". - Left fold
(... + xs):("a" + "b") + "c"="ab" + "c"="abc". The value is identical; only the intermediate temporaries differ. Contrast this with subtraction where the value itself changes — so "does direction matter?" depends on the operator's algebra.
L3.3 — Count booleans that are true
Write and trace a count_true(Bs... bs) returning how many arguments are true, using a fold.
Evaluate for count_true(true, false, true, true, false).
Recall Solution
Each bool converts to 0 or 1, so a sum fold counts the trues:
template<typename... Bs>
constexpr int count_true(Bs... bs) {
return (0 + ... + static_cast<int>(bs)); // binary left fold, safe if empty
}Trace: . Answer: 3.
We chose a binary fold with init = 0 so the empty case (count_true() = 0) still compiles.
Level 4 — Synthesis
L4.1 — Build min_of from scratch
Write a variadic min_of(a, rest...) returning the smallest argument. Trace min_of(5, 2, 8, 1, 9).
Recall Solution
A fold with a plain operator won't do min directly, but we can fold with a lambda-like helper,
or recurse. Cleanest fold uses std::min via a comma-driven update, but the clearest synthesis
is a fold over a running minimum using the ternary through a helper. Here is a recursive build
(mirrors the parent's peel-the-head pattern):
template<typename T> T min_of(T a) { return a; } // base: one left
template<typename T, typename... R>
T min_of(T a, R... rest) {
T m = min_of(rest...); // smallest of the tail
return a < m ? a : m; // compare head against it
}Trace min_of(5,2,8,1,9):
min_of(9)= 9min_of(1,9)→ m=9 → 1<9 → 1min_of(8,1,9)→ m=1 → 8<1? no → 1min_of(2,8,1,9)→ m=1 → 2<1? no → 1min_of(5,2,8,1,9)→ m=1 → 5<1? no → 1
Answer: 1. Each call peels one head and needs the base case min_of(T a) to stop.
L4.2 — A fold that also counts
Write average(xs...) returning the arithmetic mean as a double. Evaluate for average(2, 4, 9).
Recall Solution
Combine a sum fold with sizeof...:
template<typename... Ts>
double average(Ts... xs) {
return (0.0 + ... + xs) / sizeof...(xs); // safe even if... (see note)
}Trace average(2,4,9): sum ; count ; mean .
Answer: 5.0. (Guard against sizeof...(xs) == 0 before dividing in real code.)
L4.3 — Forward a pack into a tuple
Show how to store all arguments in a std::tuple preserving value categories, using
Perfect Forwarding. What is std::tuple_size of the result for pack_it(1, "x", 2.5)?
Recall Solution
template<typename... Ts>
auto pack_it(Ts&&... xs) {
return std::make_tuple(std::forward<Ts>(xs)...); // expand: forward each element
}The pattern std::forward<Ts>(xs) is repeated once per element by the trailing ..., so each
argument keeps its lvalue/rvalue nature. pack_it(1, "x", 2.5) builds a tuple of 3 elements, so
std::tuple_size<decltype(...)>::value = 3 — matching sizeof...(Ts).
Level 5 — Mastery
L5.1 — Fold with a custom operator
Given operator| overloaded so a | b returns a if a>b else b (a "max" pipe), what does
(x | ...) compute for (3, 7, 2, 9, 5)? See Operator Overloading.
Recall Solution
Right fold nests from the right, but | here returns the larger, and "larger" is associative,
so order doesn't change the value:
Answer: 9. This is how you get a variadic max in one fold line once the operator encodes
the comparison.
L5.2 — Compile-time fold
Using constexpr and Compile-time Computation, write a constexpr variadic product and
evaluate product(2, 3, 5) — and explain when the multiplication happens.
Recall Solution
template<typename... Ts>
constexpr auto product(Ts... xs) { return (1 * ... * xs); } // binary left fold, init=1. Answer: 30. Because the function is constexpr and the arguments are
literals, the compiler evaluates the whole fold at compile time — the runtime binary just
contains the constant 30. The init = 1 also makes product() (empty) return 1 safely.
L5.3 — Mixed fold, predict exact output
Trace, in order, the printed output of:
template<typename... Ts>
void dump(Ts... xs) {
std::size_t i = 0;
((std::cout << i++ << ":" << xs << ' '), ...);
}for dump("a", 42, 'Z').
Recall Solution
The comma fold evaluates the pattern left→right, and i++ advances after each print:
- element
"a": prints0:a, thenibecomes 1 - element
42: prints1:42, thenibecomes 2 - element
'Z': prints2:Z, thenibecomes 3
Output: 0:a 1:42 2:Z . Left-to-right evaluation of the comma fold is guaranteed, which is why
the index counter stays consistent.
Recall Self-test summary
sizeof...(Ts) for 4 args ::: 4
sum(10,20,30,40) via (xs + ...) ::: 100
sub(1,2,3,4) via (... - xs) ::: -8
count_true(T,F,T,T,F) ::: 3
min_of(5,2,8,1,9) ::: 1
average(2,4,9) ::: 5.0
product(2,3,5) via (1 * ... * xs) ::: 30
Empty-pack (xs + ...) ::: compile error; use (0 + ... + xs)