C++ / TEMPLATES AND GENERIC PROGRAMMING
Variadic templates and parameter packs
Write functions and classes that accept any number of arguments, expand parameter packs correctly, and consume them with sizeof... and head/tail recursion.
What you will learn
- Declare variadic function and class templates and expand packs with the ... suffix
- Get a pack's element count as a compile-time constant with sizeof...(Ts)
- Consume a pack with head/tail recursion and a non-template base-case overload
- Tell apart f(g(args)...) from f(g(args...)) and know which contexts allow expansion
Understanding Variadic templates and parameter packs
The declaration template <typename... Ts> introduces a template parameter pack: Ts is not one type, it stands for zero or more types. Writing Ts... args in the parameter list then declares a function parameter pack whose elements are deduced in lockstep with Ts from the call site. What surprises people is how little you can do with a pack directly: there is no args[0], no range-for over args, and no way to store a pack in a variable. It has no runtime identity at all; it lives in the compiler's description of the instantiation, and the only direct query is sizeof..., which yields the number of elements as a constant expression.
Everything else you do with a pack happens through expansion. You write a pattern that mentions the pack and put ... after it; the compiler copies that pattern once per element, substituting the i-th element in each copy, and joins the copies with commas. Because the result is literally a comma-separated list, expansion is only legal where the grammar already permits one: a call argument list, a braced initializer list, a template argument list, a base-specifier list, a member initializer list, a lambda capture list. The position of the ellipsis is the meaning, not decoration: h(args)... expands to h(a0), h(a1), h(a2), while h(args...) is a single call to h receiving all three.
When the elements are independent, one expansion does the job. When each element needs the result of the previous one, or needs different code, the classic technique is structural recursion: declare the template as First plus Rest..., act on First, and recurse on Rest..., with a separate non-variadic overload that terminates when nothing is left. Each recursion depth is a distinct instantiation, so the loop is unrolled at compile time; that is why a ten-argument call produces eleven functions and enormous error messages when something goes wrong. The mental model to carry forward: a pack is a compile-time list, processed either by substituting a pattern over it or by peeling it apart recursively, never by iterating it.
<iostream>
<string>
// Base case first: for the empty pack there are no arguments to trigger
// ADL, so this must be visible where the template below is defined.
void print_rest() { std::cout << "]\n"; }
template <typename First, typename... Rest>
void print_rest(const First& first, const Rest&... rest) {
std::cout << first;
if (sizeof...(rest) != 0) std::cout << ", ";
print_rest(rest...); // same call, one element shorter
}
template <typename... Ts>
void describe(const Ts&... args) {
std::cout << sizeof...(Ts) << " arg(s): [";
print_rest(args...); // hand the whole pack over
}
int main() {
describe(1, 2.5, std::string("hi"), 'x');
describe();
describe(42);
}
A parameter pack is a compile-time list with no runtime existence, so you can only ask its size or expand a pattern once per element.
Worked examples
One expansion, one pattern per element
Shows that the ellipsis repeats the entire preceding expression, not just the pack name.
<iostream>
<string>
<vector>
template <typename... Ts>
void tagged(const Ts&... args) {
std::vector<std::string> parts = { ("<" + std::to_string(args) + ">")... };
std::cout << parts.size() << ": ";
for (const std::string& p : parts) std::cout << p;
std::cout << '\n';
}
int main() {
tagged(1, 2.5, 3u);
tagged(9);
}
Example explained
Line 1The pattern is ("<" + std::to_string(args) + ">"), so the ... after it produces three complete string expressions, not one.
Line 2std::to_string is resolved separately for each element, which is why the double element comes out as 2.500000 while the unsigned prints as 3.
Line 3A braced initializer list guarantees left-to-right evaluation of the expanded elements, unlike a plain function argument list.
Line 4parts.size() equals sizeof...(args) here, but it is a runtime value, whereas sizeof...(args) is usable at compile time.
A pack inside a class template
Expands a type pack into a template argument list and a value pack into a member initializer.
<cstddef>
<iostream>
<string>
<tuple>
template <typename... Ts>
struct Row {
static constexpr std::size_t columns = sizeof...(Ts);
std::tuple<Ts...> cells;
explicit Row(Ts... vals) : cells(vals...) {}
};
int main() {
Row<int, double, std::string> r(7, 1.5, "ok");
std::cout << "columns=" << r.columns << '\n';
std::cout << std::get<0>(r.cells) << ' ' << std::get<2>(r.cells) << '\n';
Row<> nothing;
std::cout << "columns=" << nothing.columns << '\n';
}
Example explained
Line 1std::tuple<Ts...> expands the type pack into a template argument list, giving the tuple one element per column.
Line 2Because sizeof...(Ts) is a constant expression, columns can be static constexpr and read without any object.
Line 3cells(vals...) expands the function parameter pack in a member initializer list, another context where expansion is allowed.
Line 4Row<> instantiates with an empty pack: the tuple has no elements and the constructor takes no parameters.
Important notes
sizeof...(args) is the number of elements in the pack, never a total byte size; because it is a constant expression it can drive if constexpr or serve as a template argument.
Recursive peeling creates one instantiation per remaining suffix, so a ten-argument call generates eleven functions; keep the recursive body small and prefer a single expansion when the elements do not interact.
Common mistakes
Writing print(args) instead of print(args...): the pack name alone is not a value, and the compiler rejects it with "parameter pack not expanded with ...".
Defining the zero-argument base case below the recursive template: the final call has no arguments, so ADL cannot find it and lookup at the definition point fails with "no matching function for call to print()", even though the logic is correct.
Confusing f(g(args)...) with f(g(args...)): the first calls g once per element and passes N arguments to f, the second passes the whole pack to g in one call, and when both happen to compile the bug is silent.
Try it yourself
Change, predict, then run
Write template <typename... Ts> std::string joined(char sep, const Ts&... args) that returns all arguments converted with std::to_string and separated by sep, using head/tail recursion plus a base case. Check that joined(',', 5) has no trailing separator and that joined(',') returns an empty string.
Open the C++ workspaceCheck your understanding
Given template <class... Ts> void f(Ts... args) { g(h(args)...); } called with three arguments, how many times is h called and how many arguments does g receive?
- h is called three times and g receives three arguments
- h is called once with all three arguments and g receives one argument
- h is called three times and g receives one argument holding the three results
- It is ill-formed: a pack cannot be expanded inside another function call
Show answer
The ellipsis terminates the pattern h(args), so the compiler repeats that whole call expression once per element and the three results become g's argument list. Option 2 describes g(h(args...)), where the ellipsis sits inside h's argument list and the entire pack is handed to a single call of h; the results are never collected into one object, which rules out option 3.