C++ Variadic Templates and Fold Expressions: Process Type Packs Step by Step

Trace concrete type and value packs through C++11 variadic templates, C++17 folds, and a C++20 constrained function. The examples show exactly how expansion, direction, and empty packs affect the result.

KnowledgeGate Team

Exam prep & CS education

Updated 31 Oct 20265 min read
C++ Variadic Templates and Fold Expressions: Process Type Packs Step by Step

typename... Ts, Ts... values, and (... + values) all contain an ellipsis, but each declares or expands a different pack. That visual similarity is the real difficulty. Concrete packs make each expansion visible. Variadic templates arrived in C++11, fold expressions in C++17, and the requires clause in the main example is a C++20 refinement. Place variadic templates in a wider C++ learning path, or connect them to structured programming and DSA study.

1. C++ variadic templates: identify the pack before expanding it

In template <typename... Ts>, Ts is a template parameter pack containing zero or more types. In void inspect(Ts... values), values is a function parameter pack whose elements correspond to those types.

For this call:

cpp
inspect(7, 2.5, std::string{"KG"});

template argument deduction gives Ts = {int, double, std::string}. The function therefore receives three parameters holding 7, 2.5, and "KG". Both sizeof...(Ts) and sizeof...(values) evaluate to 3.

A pack is not a runtime container. You cannot access Ts[0] or values[0], and no array is created merely because several arguments were captured. Think of a pack as a compile-time sequence. C++ lets you use it only through permitted patterns, which the compiler expands once for each corresponding element.

2. Pack expansion: repeat a pattern once per element

Assume Ts = {int, double, long} and three matching function parameters named conceptually as v1, v2, and v3.

Source

Pattern

Expansion

Type pack

std::tuple<Ts...>

std::tuple<int, double, long>

Type pack size

sizeof...(Ts)

3

Value pack

consume(values)...

consume(v1), consume(v2), consume(v3) in a valid expansion context

The ellipsis expands the complete pattern immediately associated with it. In f(values...), one call receives every expanded argument: f(v1, v2, v3). By contrast, f(values)... in a permitted context repeats the whole call and forms one call per argument.

Type packs can form tuples or apply the same trait to every type, fitting the broader object-oriented and generic programming context.

A C++17 fold reduces a pack with a binary operator. With xs = {20, 5, 2}, subtraction makes the grouping direction visible:

Fold kind

Form

Expansion and result

Unary left

(... - xs)

((20 - 5) - 2) = 13

Unary right

(xs - ...)

(20 - (5 - 2)) = 17

Binary left

(100 - ... - xs)

(((100 - 20) - 5) - 2) = 73

Binary right

(xs - ... - 100)

(20 - (5 - (2 - 100))) = -83

The outer parentheses are mandatory. Here, pack means an expression containing the unexpanded pack, such as xs. A binary fold also requires the same operator on both sides of ...; mixing operators does not form a legal fold expression.

Addition often hides the choice because both directions agree. Subtraction, division, string composition, and user-defined operators can produce different answers, so choose direction deliberately.

Four fold-expression trees compare left- and right-associative subtraction over 20, 5, and 2, including binary folds initialized to 100.

4. Worked example: validate a type pack, fold its values, then scale the result

This C++20 function first constrains every captured type, then converts and adds every value:

cpp
template <typename... Ts>
  requires (std::is_arithmetic_v<Ts> && ...)
double add_scaled(double scale, Ts... values) {
    return scale * (0.0 + ... + static_cast<double>(values));
}

Trace add_scaled(1.5, 4, 2.5, 3u) line by line:

  1. The first argument binds to scale. Deduction gives Ts = {int, double, unsigned int} for the remaining three arguments.

  2. The constraint expands to true && true && true, so the function is viable.

  3. The binary left fold expands to ((0.0 + 4.0) + 2.5) + 3.0. Its running totals are 4.0, 6.5, and 9.5.

  4. Scaling gives 1.5 * 9.5 = 14.25.

The initial value handles an empty pack: add_scaled(1.5) computes 1.5 * 0.0 = 0.0. In contrast, add_scaled(1.5, 4, std::string{"2"}) is rejected because its constraint becomes true && false.

In C++17, where requires is unavailable, place static_assert((std::is_arithmetic_v<Ts> && ...), "all arguments must be arithmetic"); inside the function body. The fold expressions themselves remain valid C++17.

A two-lane flow shows add_scaled deducing three arithmetic types, folding values to 9.5, and multiplying by 1.5 to produce 14.25.

5. Useful fold patterns: all, any, output, and forwarding

Logical folds express pack-wide checks clearly. (std::is_integral_v<Ts> && ...) is true for {int, unsigned int, long} but false for {int, double, long}. Similarly, (std::is_pointer_v<Ts> || ...) is true for {int, double*, char} because double* satisfies the trait.

A comma fold is useful when each operation must run in sequence:

cpp
std::size_t index = 0;
((std::cout << (index++ ? ", " : "") << values), ...);

With values 4, "KG", and 2.5, it prints exactly 4, KG, 2.5. The comma operator sequences each operation from left to right.

With forwarding-reference parameters Ts&&... values, production code often uses (process(std::forward<Ts>(values)), ...). Forward each argument once when process may consume it. Forwarding and consuming the same rvalue pack twice can make the second use observe moved-from objects.

6. Variadic template and fold-expression traps

Mistake

Why it fails

Correction

Omitting the fold's outer parentheses

Fold grammar requires them

Write (... + values)

Expecting (... + values) to accept an empty pack

Unary + has no empty-pack identity

Use (0 + ... + values)

Selecting a right fold when order matters

It groups operands from the other direction

Expand a small non-associative example first

Assuming + sequences side effects

Grouping does not supply the needed sequencing

Use a comma fold for ordered actions

Treating the type count as an array length

A pack is not a runtime array

Use sizeof...(Ts) only as the compile-time pack size

For an empty pack, a unary && fold yields true, a unary || fold yields false, and a unary comma fold yields void(). Other unary operators cannot fold an empty pack. Use a binary fold with an initial value, such as (0 + ... + values), when empty input must work.

A fold fixes parenthesisation, not the sequencing of arithmetic side effects. Use the comma operator when actions must run left to right.

7. How C++ variadic templates are tested in interviews and assessments

Use a four-part trace drill for add_scaled(1.5, 4, 2.5, 3u): identify int, double, and unsigned int; compute sizeof...(Ts) = 3; expand the constraint to true && true && true; then calculate 14.25. Next, explain why the call containing std::string{"2"} fails before runtime.

Direction is a separate check. For xs = {20, 5, 2}, a correct explanation distinguishes the unary-left result 13 from the unary-right result 17; listing the same operands is not enough. For extra implementation practice, take one exercise from Classic Programs in C, Java and Python and rewrite a repeated operation as a type-safe fold.

8. C++ variadic templates and folds: the short version

Identify the type or value pack, mark the pattern that repeats, then choose fold direction and an initial value deliberately. The memory check is 3 deduced types, a 9.5 folded sum, and a 14.25 scaled result. For an organised language sequence and practice, continue with C++ Programming: Concepts, MCQs, Coding Questions. Otherwise, implement the drills from sections 5 and 7.