5.2.16 · D4C++ Programming

Exercises — Variadic templates — parameter packs, fold expressions

2,356 words11 min readBack to topic

Before we start, one picture pins down the vocabulary we reuse in every problem.

Figure — Variadic templates — parameter packs, fold expressions

Level 1 — Recognition

L1.1 — Read the ellipsis

State, for each line, whether ... declares a pack or expands one:

  1. template<typename... Ts>
  2. void f(Ts... args)
  3. g(args...)
  4. (args + ...)
Recall Solution

Our reading key: ... before the name declares; ... after a pattern expands.

  1. typename... Ts... is before Tsdeclare a type pack.
  2. Ts... args... is before argsdeclare a value pack.
  3. args...... follows the pattern argsexpand (produces a0, a1, a2, ...).
  4. (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?

  1. (xs + ...)
  2. (xs && ...)
  3. (xs || ...)
  4. (xs, ...)
  5. (0 + ... + xs)
Recall Solution

Only three unary folds have a defined identity for an empty pack; the rest are compile errors.

  1. (xs + ...)compile error (no identity element for + in a unary fold).
  2. (xs && ...)compiles, value true.
  3. (xs || ...)compiles, value false.
  4. (xs, ...)compiles, value void() (nothing).
  5. (0 + ... + xs)compiles, value 0. This is a binary fold whose init = 0 supplies 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) = 9
  • min_of(1,9) → m=9 → 1<9 → 1
  • min_of(8,1,9) → m=1 → 8<1? no → 1
  • min_of(2,8,1,9) → m=1 → 2<1? no → 1
  • min_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": prints 0:a , then i becomes 1
  • element 42: prints 1:42 , then i becomes 2
  • element 'Z': prints 2:Z , then i becomes 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)