Variadic templates, added in C++11, let a single template accept any number of arguments of any types. They replace the old pattern of writing one overload per arity, and combined with fold expressions from C++17, they make operating on an arbitrary list of arguments straightforward.
Before C++11, a function that worked with one argument, two arguments, three arguments, and so on required one overload per arity. Consider a logEvent helper that takes a level plus some message parts and joins them.
The overload list stops at some hard-coded limit, say four or five arguments, and any caller who needs more is stuck. The other escape hatch was a C-style variadic function with ... and va_list, but those lose all type safety: the compiler cannot check that every argument is a string, and a wrong format specifier is undefined behavior. Some libraries used macros, generating overloads up to twenty arguments. Painful, brittle, hard to debug.
A variadic template solves all three problems at once: one definition, any number of arguments, full type checking.
A variadic template introduces a template parameter pack, a placeholder for zero or more template parameters. The syntax uses typename... (or class...) followed by a name.
Read that as: "for any list of types Args, here is a function taking one parameter of each type, collectively named items." Args is the template parameter pack, and items is the matching function parameter pack.
The compiler decides what Args is at the call site based on the arguments passed. The call printOrderItems("Pen", 2, 9.99) causes the compiler to infer Args = const char*, int, double and produce three parameters in items.
A pack is used by expanding it with the ... operator. The compiler stamps out a copy of the pattern to the left of ... for each element in the pack, separated by commas.
The most common place to expand is when forwarding the pack to another function.
The pattern can be more than just the pack name. Writing printOne(items + 1)... causes the compiler to emit printOne(items_0 + 1), printOne(items_1 + 1), .... The pattern is whatever sits to the left of the ..., with the pack expanded one element at a time.
sizeof... for Pack SizeThe number of elements in a pack is available via sizeof...(name). It returns a std::size_t known at compile time, and it works on both template parameter packs and function parameter packs.
sizeof...(Args) and sizeof...(discounts) give the same number, since the type pack and the value pack always line up.
Before C++17 added fold expressions, the usual way to consume a pack was recursion: peel off the first argument, recurse on the rest, and provide a base case. The pattern still shows up and in interview questions.
The trailing [] line is the base case firing when rest is empty; it prints the level with nothing after it. That can be suppressed by checking sizeof...(rest) and only printing the trailing newline at the end, but the structure is what matters here.
The flow.
Each call consumes one argument from the pack until the base case matches. The compiler generates a separate function instantiation for each step, so this is purely a compile-time recursion: the runtime call chain looks normal, but every layer has its own type signature.
Each recursive step produces a new function instantiation, which means more code in the binary and slower compile times for deep packs. Fold expressions (next section) generate one expansion instead.
C++17 introduced fold expressions, a cleaner way to reduce a pack with a binary operator. There are four forms, and they all live inside parentheses.
| Form | Syntax | Expansion |
|---|---|---|
| Unary right fold | (pack op ...) | e_1 op (e_2 op (... op e_n)) |
| Unary left fold | (... op pack) | ((e_1 op e_2) op ...) op e_n |
| Binary right fold | (pack op ... op init) | e_1 op (e_2 op (... op (e_n op init))) |
| Binary left fold | (init op ... op pack) | ((init op e_1) op e_2) op ... op e_n |
The ... always sits between the operator(s). The pack is on whichever side has no init. Right vs left controls how parentheses group, which matters for non-associative operators like subtraction and stream output.
For commutative and associative operators like +, the left and right forms give the same number, but the difference matters elsewhere. (std::cout << ... << items) works because << is left-associative: each insertion returns the stream, which the next << consumes. A right fold over << would group as items_0 << (items_1 << (items_2 << ...)), which has the wrong type since the rightmost group is a stream-to-int expression.
How a binary left fold over << unfolds for printAll("a", 1, "b").
Each step keeps the stream as the left operand and folds the next pack element in. The result of the whole expression is the stream itself, which is why << "\n" can be appended after it.
Unary folds with an empty pack are an error for most operators. The only operators allowed on an empty unary fold are && (yields true), || (yields false), and the comma operator (yields void()). For everything else, supply an init value with a binary fold.
Picking a sensible identity (0 for +, 1 for *, the empty string for string concatenation) is usually enough to make a fold expression total over any pack size.
The same mechanism works on classes. The canonical example is std::tuple, a fixed-size heterogeneous list of values.
A variadic class can also be user-defined. A shopping Cart that stores any combination of item types is a small example.
The expansion Items... inside std::tuple<Items...> means "stamp out the type list one by one and pass them as template arguments to tuple". The same pattern works wherever a type list is expected: base class lists, template parameters, function call arguments.
Variadic templates pair naturally with forwarding references (also called universal references). A function parameter pack declared as Args&&... deduces each parameter's value category independently: an lvalue argument deduces to an lvalue reference, an rvalue argument to an rvalue reference.
To preserve those categories when calling another function, use std::forward<Args>(args).... This is the perfect forwarding pattern. (Forwarding references and std::forward get their full treatment in the Modern C++ Features section. They're used here.)
The pattern std::forward<Args>(args)... is itself a pack expansion: the compiler expands it once per element, so for three arguments it becomes std::forward<Args_0>(args_0), std::forward<Args_1>(args_1), std::forward<Args_2>(args_2). The pack on the type side (Args) and the pack on the value side (args) are expanded together because they appear in the same pattern.
This is how std::make_unique, std::make_shared, emplace_back, and similar factories accept "any constructor arguments" and pass them through unchanged.
Fold expressions are almost always shorter and faster to compile, but they can't do everything. The rule of thumb.
| Situation | Use |
|---|---|
Reducing with a single binary operator (+, &&, <<, ,) | Fold expression |
| Calling a function on each pack element | Comma fold: (f(args), ...) |
| Building one expression from all elements | Fold expression |
| Different operation per element type | Recursion (or if constexpr inside a fold) |
| Producing intermediate results between elements | Recursion |
| Pre-C++17 codebase | Recursion (folds need C++17) |
The comma-fold trick (f(args), ...) is worth highlighting: it doesn't compute a result, it calls f on each element in order, sequenced by the comma operator. That covers most "do this to every argument" needs without writing a recursive base case.
Use recursion when the per-element logic differs by position (first element treated specially, last element gets a separator skipped), or when an intermediate state flows from one element to the next in a way a single operator can't express.
10 quizzes