Skip to content
VLSI Mentor

C++ Part I · Module 18 · Templates and Generic Programming

Variadic Templates and Parameter Packs

Every template so far has had a fixed number of parameters, and several facilities this curriculum has already relied on cannot be written that way. Chapter 14.4 used make_unique about thirty times across Module 14 — a function that takes whatever arguments the constructed type's constructor takes, however many there are and whatever their types — and explained the leak it closes without ever explaining how it accepts an arbitrary argument list. A container's emplace does the same. So does any logging call that takes a message and some values without resorting to the unchecked variadic machinery inherited from C. This chapter is the mechanism that makes all three possible: a template parameter that stands not for one type but for a list of them, of any length including zero. It covers how to count a pack, how to use its contents, and the C++17 addition that collapses what used to be a recursive pair of functions into a single expression — both forms measured side by side on the same arguments. It closes with the forwarding idiom that make_unique is built from, which combines this chapter's packs with the deduction rule from Chapter 18.3 that looked like a curiosity at the time. And it records a mistake: the program that produced these measurements failed to compile on the first attempt, for a reason the previous-but-one chapter had already established.

Advanced18 min readC++Part IFundamentalsTemplates

Part I · Module 18 · Chapter 18.9 · Variadic Templates and Parameter Packs

1. The Limitation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename T> std::unique_ptr<T> myMakeUnique();                  // 0 args
    template <typename T, typename A> std::unique_ptr<T> myMakeUnique(A);     // 1
    template <typename T, typename A, typename B> std::unique_ptr<T> myMakeUnique(A, B);

That is Chapter 18.1 §2's per-type copying problem again, in a new dimension: one overload per arity. The standard library would need an arbitrary number of them, and a constructor taking five arguments would need the fifth.

The C inheritance — ... with va_arg — removes the arity limit and throws away the types, which Chapter 18.1 §3 measured going wrong for void*: a mismatch compiles and returns a wrong answer.

2. A Parameter Pack

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename... Ts>
    std::size_t howMany(const Ts&...) { return sizeof...(Ts); }

Three uses of ..., each doing something different, and keeping them straight is most of the syntax:

WhereMeaning
typename... Tsdeclares a template parameter pack — Ts names a list of types
const Ts&...expands the pack into a parameter list
sizeof...(Ts)the number of elements in the pack
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. sizeof...(Ts):
     howMany()              = 0
     howMany(1,2,3)         = 3
     howMany(1,"a",2.5,'c') = 4

Zero is allowed, and the types need not be related — int, const char*, double, char in one call. sizeof... is a compile-time constant, usable everywhere Chapter 18.5 §6's constants are.

3. Using the Contents: The Recursive Form

Before C++17 the only general way to process a pack was recursion, peeling one argument at a time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void legacyPrint() { std::printf("\n"); }                       // the base case

    template <typename T, typename... Rest>
    void legacyPrint(const T& first, const Rest&... rest)
    {
        std::printf(" <%s>", toStr(first).c_str());
        legacyPrint(rest...);                                              // one fewer each time
    }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D. the recursive form:
     <1> <2.5> <three>

Two functions for one job, and the base case is a separate non-template overload that terminates the recursion. legacyPrint(rest...) expands the remaining pack as the new argument list, so each call has one fewer argument until the pack is empty.

It works, it is what pre-C++17 code looks like, and §4 is why you will rarely write it now.

4. Fold Expressions (C++17)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename... Ts> auto sum(const Ts&... xs) { return (xs + ...); }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. a C++17 fold expression:
     sum(1,2,3,4)  = 10
     sum(1.5, 2.5) = 4.0

One line, no recursion, no base case. (xs + ...) expands to x1 + (x2 + (x3 + x4)) — the compiler writes the chain.

Four forms exist, and the difference matters for non-associative operators:

FormExpands to
(xs + ...)unary right fold — x1 + (x2 + x3)
(... + xs)unary left fold — (x1 + x2) + x3
(init + ... + xs)binary left fold — ((init + x1) + x2)
(xs + ... + init)binary right fold — x1 + (x2 + init)

The binary forms accept an empty pack; the unary forms generally do not, which is the practical reason to prefer (0 + ... + xs) over (xs + ...) when zero arguments is legal.

And a fold over the comma operator performs an action for each element:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename... Ts>
    void printAll(const Ts&... xs) { ((std::printf(" [%s]", toStr(xs).c_str())), ...); }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C. a comma fold over the pack:
     [1] [2.5] [three]

That is the general-purpose idiom — any expression, repeated once per element, in order. It replaces §3's recursion for almost every case, and it is the one piece of this syntax worth memorising.

(The comma here is the built-in operator, with its guaranteed left-to-right sequencing — Chapter 15.7 §4 measured what overloading it would destroy.)

5. Variadic Class Templates

The same parameter can appear on a class:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename... Ts> class Tuple;          // the declaration std::tuple has

That is the shape behind std::tuple, and using it well needs machinery — index sequences, std::apply — that belongs with the utility types in Module 21. The declaration is worth recognising now; a hand-written Tuple is not a good use of this chapter.

6. Perfect Forwarding: The make_unique Shape

The combination this chapter exists for. Chapter 14.4 used std::make_unique throughout Module 14; here is what it is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename T, typename... Args>
    std::unique_ptr<T> myMakeUnique(Args&&... args)
    {
        return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
E. perfect forwarding (the make_unique shape):
     Widget(7, "seven", 1.5)

Three arguments of three different types reached Widget's constructor unchanged. Four pieces combine, and each has already been established:

typename... Args — any number of arguments (§2).

Args&&... — a pack of forwarding references. Chapter 18.3 §5 measured the rule: for an lvalue argument Args deduces a reference type, for an rvalue it deduces the plain type, and reference collapsing makes the parameter match whatever was passed. Applied per element of the pack.

std::forward<Args>(args)... — passes each one on preserving its category. Chapter 13.9 §4 listed passing on a T&& parameter as one of the three places a cast belongs; std::forward is the version that preserves rather than always claiming rvalue.

And new T(...) inside the function — which is Chapter 14.4 §3's point: the allocation and the assumption of ownership happen together, closing the window in which an unowned raw pointer exists. Chapter 14.4 §3 measured the alternative leaking.

So make_unique is a variadic forwarding wrapper whose only job is to shrink that window, and the variadics are what let it do so for a constructor of any arity.

7. Where Else This Appears

FacilityWhat the pack is for
std::make_unique<T>(args...)the constructor's arguments — Chapter 14.4
std::make_shared<T>(args...)the same, plus Chapter 14.5 §6's single allocation
vector::emplace_back(args...)construct in place rather than construct-then-move — Module 19
std::tuple<Ts...>the element types — Module 21
a type-safe log callthe values to format, with their types intact

That last one is the frozen description's third example and worth a line, because it is the one you might write yourself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename... Ts>
    void logLine(const char* tag, const Ts&... values)
    {
        std::printf("[%s]", tag);
        ((std::printf(" %s", toStr(values).c_str())), ...);
        std::printf("\n");
    }

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. a variadic log call -- mixed types, each resolved by overload:
[WRITE] 64 2.50 burst true
[READ] 7
[TICK]

Four values of four different types, one call, each formatted by its own overload — and logLine("TICK") with an empty pack works too.

The type-safety claim is worth measuring against the alternative. A mismatched printf is caught here only by a warning:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::printf("%d\n", 2.5);
  format specifies type 'int' but the argument has type 'double' [-Wformat]

That diagnostic exists because clang special-cases printf's format strings; for your own printf-style function it would not. Whereas an unsupported type in the variadic version is an ordinary overload failure:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logLine(Weird{});
  error: no matching function for call to 'toStr'

A compile error rather than a warning, and from the ordinary rules rather than a compiler special case. That is the whole argument for a variadic logging interface over the C one: every value keeps its type, so toStr is resolved by overload resolution per element, and a type you forgot to support is a build failure instead of undefined behaviour at run time.

The initializer_list alternative from §9, for contrast:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. the initializer_list version -- one type, braces at the call:
[ADDRS] 1 2 3 4

Simpler, and homogeneous only — {1,2,3,4} must all be int. Where that is true it is the better choice; the pack earns its complexity exactly when the types differ.

8. The Mistake I Made Writing This

The program producing §2–§6's output did not compile the first time, and the reason is Chapter 18.6 §7's:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: call to function 'toStr' that is neither visible in the template definition
       nor found by argument-dependent lookup
note: in instantiation of function template specialization 'show<int>' requested here
note: 'toStr' should be declared prior to the call site

I had defined the three toStr overloads below the template that calls them. toStr does not depend on a template parameter in the way that would defer its lookup, so Chapter 18.6 §3's phase 1 looked for it at the point of definition — and it was not yet declared.

Moving the overloads above the template fixed it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  helper(42)

Two things worth taking from this. The diagnostic names its own remedy — should be declared prior to the call site — which is rare enough in template errors to be worth noticing. And this is the most likely way a variadic helper will fail for you, because a pack-processing template almost always calls a helper per element, and the helpers are the kind of small function one naturally writes near the bottom of a file.

9. When Not to Use a Pack

Three cases, because variadics attract over-use.

When the arity is genuinely fixed. Two arguments is two parameters; a pack adds compile time and obscures the signature.

When the arguments are homogeneous. For "any number of the same type", std::initializer_list<T> is simpler and gives the caller braces — sum({1,2,3}) — at the cost of requiring one type.

And when you are reaching for recursion. If a fold expression will not express it, consider whether the function wants a container or a range instead. Pre-C++17 code is full of recursive pack processing because there was no alternative; there is now.

(std::tuple and std::apply are the tools for genuinely heterogeneous collections that must be stored rather than consumed immediately, and they are Module 21's.)

10. What to Actually Do

Use typename... Ts to declare, Ts... to expand, sizeof...(Ts) to count.

Prefer a fold expression to recursion — one line, no base case.

Use a comma fold ((expr), ...) for per-element actions.

Prefer the binary fold forms when an empty pack is legal.

Use Args&&... plus std::forward<Args>(args)... for any wrapper that passes arguments on.

Declare per-element helpers above the template — Chapter 18.6 §7, and §8's mistake.

Use std::initializer_list<T> for homogeneous arguments, and a fixed parameter list for fixed arity.

And reach for std::tuple only when the pack must be stored rather than consumed.

11. Mistakes Worth Making Once

One overload per arity. Chapter 18.1 §2's copying problem in a new dimension.

Falling back on C's ... and va_arg. The types are gone — Chapter 18.1 §3's measured wrong answer.

Confusing the three ... positions. Declare, expand, count.

A unary fold on a possibly-empty pack. Use the binary form with an initial value.

Writing recursion where a fold would do. Measured: identical output, one line instead of two functions.

Omitting std::forward. Every argument arrives as an lvalue; move-only types will not pass.

Taking const Args&... in a forwarding wrapper. Everything is copied and nothing can be moved.

Defining per-element helpers below the template. Measured — this is my own diagnostic in §8.

Using a pack for two fixed arguments. Compile time and obscurity for nothing.

12. Interview Relevance

"What is a parameter pack?" A template parameter standing for any number of types, declared typename... Ts, expanded Ts..., counted sizeof...(Ts).

"How do you process one?" A C++17 fold expression, or recursion with a base case before C++17. Measured producing identical output.

"What is a comma fold for?" An action per element — ((expr), ...) — the general-purpose idiom.

"Why prefer the binary fold forms?" They accept an empty pack.

"Write make_unique." template <typename T, typename... Args> std::unique_ptr<T> f(Args&&... a) { return std::unique_ptr<T>(new T(std::forward<Args>(a)...)); } — and the strong answer names what each piece is for.

"What breaks if you drop std::forward?" Everything arrives as an lvalue — move-only types will not pass, movable ones are copied.

"Why is new T(...) inside the function rather than outside?" It closes the window in which an unowned raw pointer exists — Chapter 14.4 §3's measured leak.

13. What You Should Be Able to Say Now

  1. A fixed-arity template needs one overload per argument count; C's ... removes the limit and the type safety.
  2. typename... Ts declares a pack, Ts... expands it, sizeof...(Ts) counts it.
  3. Measured: howMany() = 0, howMany(1,2,3) = 3, howMany(1,"a",2.5,'c') = 4 — zero allowed, types unrelated.
  4. sizeof... is a compile-time constant.
  5. The pre-C++17 way to process a pack is recursion with a separate base case — measured producing <1> <2.5> <three>.
  6. Measured: a C++17 fold did it in one line — sum(1,2,3,4) = 10, sum(1.5,2.5) = 4.0.
  7. Four fold forms exist, and the binary ones accept an empty pack.
  8. A comma fold ((expr), ...) performs an action per element — measured [1] [2.5] [three].
  9. The built-in comma's left-to-right sequencing is what makes that ordered — Chapter 15.7 §4.
  10. A class may take a pack — template <typename... Ts> class Tuple; — which is std::tuple's shape.
  11. Measured: the forwarding shape passed three differently-typed arguments through to Widget(7, "seven", 1.5).
  12. Args&&... is a pack of forwarding references — Chapter 18.3 §5's deduction, per element.
  13. std::forward<Args>(args)... preserves each argument's category; omitting it makes everything an lvalue.
  14. new T(...) inside the wrapper closes Chapter 14.4 §3's leak window.
  15. Measured, my own mistake: helpers defined below the template gave neither visible in the template definition nor found by argument-dependent lookup — Chapter 18.6 §7's phase-1 rule.
  16. Do not use a pack for fixed arity, homogeneous arguments, or where a fold will not express the job.

14. Exercises

Exercise 1 — the three dots. Write a function template taking any number of arguments and returning how many, and label each use of ....

Exercise 2 — fold it. Write allTrue(...) returning whether every argument is truthy, using a fold, and say which form you chose and why.

Exercise 3 — empty packs. Say what (xs && ...) and (true && ... && xs) do for zero arguments.

Exercise 4 — per element. Write a function printing each argument on its own line using a comma fold.

Exercise 5 — recursion for contrast. Write the same function recursively, and count the lines.

Exercise 6 — forward it. Write myMakeShared<T>(args...) and say what each of the four pieces contributes.

Exercise 7 — break it. Remove std::forward and say what fails, then take const Args&... and say what else does.

Exercise 8 — my mistake. Write a variadic printer calling a helper declared below it, give the diagnostic, and give two fixes.

Exercise 9 — not a pack. For three designs, say whether a pack, an initializer_list, or a fixed parameter list fits.

15. Summary

  • A parameter pack stands for any number of types — declared typename... Ts, expanded Ts..., counted sizeof...(Ts).
  • Measured: 0, 3 and 4 elements, with unrelated types in one call.
  • Measured: recursion and a C++17 fold produced the same output, and the fold was one line.
  • The binary fold forms accept an empty pack; the unary ones generally do not.
  • A comma fold is the general per-element idiom — measured [1] [2.5] [three].
  • Measured: the make_unique shape forwarded three differently-typed arguments through to the constructor.
  • It needs all four pieces — the pack, Args&&..., std::forward, and the new inside the function (Chapter 14.4 §3's leak window).
  • Measured, my own mistake: per-element helpers must be declared above the template — Chapter 18.6 §7.
  • Do not use a pack for fixed arity or homogeneous arguments.

Every mechanism in this module is now in place, and one question has been deferred nine times. Each chapter has measured what a facility does and what it costs, and none has asked when a template is the right answer at all — which the module's last chapter has to, because the costs are real and they are not the ones people watch. Code size grows once per instantiation, measured in the first chapter at a hundred and eighty bytes becoming eight hundred and forty-three. Compile time grows the same way. A requirement that cannot be stated in a declaration has to live in a comment. And a diagnostic can reach three hundred and seventy lines. The closing chapter is the judgement: writing against the operations a type must support rather than against a named type, stating those operations where the compiler will quote them back, and recognising the cases — more common than the enthusiasm for templates suggests — where the generic version is more complicated than the two concrete ones it replaced.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.