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

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:
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 |
|
|
Type pack size |
|
|
Value pack |
|
|
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.
3. C++ fold expressions: the four legal shapes and their direction
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 |
|
|
Unary right |
|
|
Binary left |
|
|
Binary right |
|
|
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.

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:
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:
The first argument binds to
scale. Deduction givesTs = {int, double, unsigned int}for the remaining three arguments.The constraint expands to
true && true && true, so the function is viable.The binary left fold expands to
((0.0 + 4.0) + 2.5) + 3.0. Its running totals are4.0,6.5, and9.5.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.

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:
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 |
Expecting | Unary | Use |
Selecting a right fold when order matters | It groups operands from the other direction | Expand a small non-associative example first |
Assuming | 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 |
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.
Keep learning
Templates in C++: Reusable Parameterized Code with Worked Examples
Learn how one C++ template produces type-specific functions and classes. Follow exact calls, calculations, deduction failures, and practical corrections.
Friend Functions in C++: Controlled Class Access with a Worked Example
See how a non-member friend function can validate and update two Wallet objects while private balances stay hidden from ordinary callers. Includes runnable C++ and an exact trace.
Data Types in C++: Fundamental and Derived Types with Worked Examples
Learn how C++ fundamental types differ from arrays, pointers, references, and function types. Trace one complete program, then test the conversion traps.
C++ Constructors and Object Initialization: Techniques, Order and a Worked Trace
Learn how C++ chooses default, parameterised, delegating and copy constructors, then trace member states and initialization order through complete code.





