Concepts / Template Argument Deduction

Template Argument Deduction

A parameter pack can hold zero, one, or many arguments, and the arguments in it do not have to share a type. The number and types are fixed by each call, not by the template declaration.

  • Programming

Why a Fixed Signature Falls Short

Most functions you have written so far have a fixed signature. You decide how many parameters they take and of what types, and every call has to match. Some tasks do not fit that pattern. A print routine, for example, should be able to accept one value, three values, or ten, and those values may have different types: an integer, a floating-point number, a string literal, a character. This article shows how C++ variadic templates let one function definition accept such an open-ended list. It also shows how the compiler works out the template arguments it needs from the values you pass in.

Consider a program whose main function makes two calls to a single function named f. The first call passes an integer, a double, and a string. The second call passes a double, a character, a string, and three integers. Both calls use the same function. Before you look at how that function works internally, commit to a prediction about what the first line of output will be.

What do you think happens?

For the call f(1, 2.2, "hello"), what does the first line of output look like?

Reveal answer
Answer:

The line reads 1 2.2 hello followed by a trailing space, because the helper writes a space after every value, including the last one.

Each value is printed in the order it is consumed, and the helper always follows a value with a space. The final space is easy to miss. The newline comes from main after the call returns, not from the helper.

If your guess matched, you already have the right intuition. If it did not, the trace will show where your mental model differed from what the code does. Watch the execution closely, because the important detail is not only what gets printed but the order in which the function calls happen to produce it.

Declaring a Parameter Pack

Definition

A variadic template is a template that accepts zero or more arguments of types that are not fixed in advance. The declaration places an ellipsis after the type parameter name, written as typename... Args. This group of type parameters is called a parameter pack. The parameter pack is paired with a function parameter pack, written as Args... args, so that the function itself receives however many values the caller supplies.

The same definition therefore accepts very different calls. The source uses two of them as examples: f(1, 2.2, "hello") passes three arguments of three different types, and f(0.2, 'c', "yuck!", 0, 1, 2) passes six arguments mixing floating-point, character, string, and integer values. Both are valid calls to the same template, and neither needs its own overload.

Key point

A parameter pack can hold zero, one, or many arguments, and the arguments in it do not have to share a type. The number and types are fixed by each call, not by the template declaration.

Splitting Off the Head

Declaring a pack is only half of the technique. To do something useful with each value, a common approach is to split the arguments into a first value and the rest. The first value is handled directly, and the remaining values are passed to the same function again. Each recursive call therefore has one fewer argument than the call before it. The function calls itself until no arguments remain, and that empty case is the base case that stops the recursion.

The diagram below follows the first call from main. The outermost call is the only one main makes. Every call beneath it exists because the call above it passed along its tail. The deepest call receives nothing and is handled by the base case.

f(1, 2.2, hello)head = 1, tail = 2.2, hellof(2.2, hello)head = 2.2, tail = hellof(hello)head = hello, tail emptyf()base case, recursion stopspasses tailpasses tailpasses empty tail
At each recursive call, which argument has been removed from the parameter pack, and what remains for the next call?

Read the chain from the top. The work for each value happens at a specific level of this chain, and the order in which those levels are reached determines the order of the printed values. The recursion itself does not decide what to do with a value. It only decides which value is next.

Writing the Recursive Form

The recursive form uses a head parameter followed by a pack named tail. Its signature is template<typename T, typename... Args> void f(T head, Args... tail). When the call f(1, 2.2, "hello") is made, head receives 1, and tail holds 2.2 and "hello". The function then calls f on the tail, and so on. The code below is an illustrative sketch that follows this signature, with the helper and the base case written out so the recursion is complete.

python
Output (expected)
1 2.2 hello 
Notes on the code
  • The base case f() must be declared before the variadic template so the empty call has a target.
  • g prints one value followed by a space; it is the only place output is written.
  • f(tail...) expands the remaining pack into a call with one fewer argument.
  • The newline is written by main after f returns, not inside f.

The Helper That Prints Each Value

The recursion is not responsible for printing. A second function, which the source names g, takes one argument and writes it to standard output followed by a single space. Because g is also a template, it works for an integer, a double, a character, or a string literal without any change.

The reason g is a separate function is that each function has one clear job. The recursive function walks the argument list. The helper prints a single value. If you later want the values separated by commas instead of spaces, or want them written to a file, only the helper needs to change. The recursion structure stays the same. The source describes the whole arrangement as a simple variant of printf that prints arbitrary lists of values, and notes that the essential ingredients are the head and tail split, the call on the tail, and the empty base case.

Tracing the First Call Step by Step

Worked example

Tracing f(1, 2.2, hello)

Trace which value is printed at each level when main calls f(1, 2.2, "hello"), and state the exact output line.

  1. 1
    Call 1

    head is 1 and tail is 2.2, hello. g(head) prints 1 followed by a space. Then f is called on the tail.

  2. 2
    Call 2

    head is 2.2 and tail is hello. g(head) prints 2.2 followed by a space. Then f is called on the tail.

  3. 3
    Call 3

    head is hello and tail is empty. g(head) prints hello followed by a space. Then f is called on the empty tail.

  4. 4
    Call 4

    The empty call f() matches the base case and does nothing. The recursion ends and control returns up the chain.

  5. 5
    Newline

    main writes the newline after f returns.

Result

The line is 1 2.2 hello followed by a trailing space and then a newline.

Pack at call 12.2, helloPack at call 2helloPack at call 3emptyhead 2.2 consumedhead hello consumed
How does the contents of the parameter pack change from one recursive step to the next until it reaches the empty pack?

Predicting a Longer Call

The second call in the program passes more arguments, so it shows the pattern more clearly. Its arguments are 0.2, 'c', "yuck!", 0, 1, and 2. The same rule applies at each level: the head is printed, then the function is called on the tail, which is one argument shorter.

What do you think happens?

Before reading on, what is the printed line for f(0.2, 'c', "yuck!", 0, 1, 2)?

Reveal answer
Answer:

0.2 c yuck! 0 1 2 followed by a trailing space.

Each argument is consumed from the front of the pack, one per call, and the helper prints each value followed by a space. The order of the printed values matches the order of the arguments.

How the Compiler Deduces Template Arguments

Definition

Template argument deduction lets the compiler determine template arguments from the function arguments in a call. In the source's example, the template parameter Source is deduced to be double from the argument 1.2, so the caller does not have to state it.

The diagram traces the two sources of information for a call to the conversion function template named to. The argument 1.2 feeds the deduction of Source. The default supplies Target. Both feed into the fully resolved specialization.

1.2function argument at callsiteSourcededuced as doubleDefaultdefault template argumentTargetdefaulted to stringdeducessupplies
Which argument type at the call site maps to which template parameter, and which parameter comes from a default instead?

Explicit, Defaulted, or Deduced

A function template argument needs to be written explicitly only when it cannot be deduced or when it has no default. Each template parameter in a call therefore comes from one of three places: it is written by the caller, deduced from a function argument, or supplied by a default. Knowing which of the three applies is the key to reading any call.

Locating a Surprising Result

When a result is unexpected, check each template parameter in turn to see whether it was written, deduced, or defaulted. This keeps you from guessing. Once you know which of the three sources produced the parameter you did not expect, you know which part of the call to change.

For the to family in the source, the Target parameter is the one that comes from the default. A mismatch in Target therefore points to the default, while a mismatch in Source points to the argument that was deduced.

Common Mistakes

  • Common mistake

    Leaving out the zero-argument base case

    What it looks like

    A variadic f that only has the head and tail version, with no f() overload.

    Why it fails

    The recursion passes the tail along until the pack is empty, and there is then no call that accepts an empty pack to stop the chain.

    Do this instead

    Declare a zero-argument f that ends the recursion before the variadic template calls f(tail...).

  • Common mistake

    Looking for a line break in the value output

    What it looks like

    Expecting the line to end cleanly at hello, with no trailing space.

    Why it fails

    The helper writes a space after each value, including the last one, so the line ends with a space before main writes its newline.

    Do this instead

    Compare the trace to the output character by character, and treat the trailing space as part of the expected output.

  • Common mistake

    Assuming a mismatch in the printed order means the values were reordered

    Why it fails

    The values are printed in the order the recursion reaches them. The order of the arguments in the call is preserved.

    Do this instead

    Follow the head of each call in sequence and check the output as it is written.

  • Common mistake

    Assuming every template argument must be written

    What it looks like

    Writing to<double, string>(1.2) when to(1.2) already resolves to the same specialization.

    Why it fails

    Deduction fills in arguments that can be determined from the function arguments, and defaults fill in the rest.

    Do this instead

    Write an argument only when it cannot be deduced and has no default, or when you want to override a default.

Practice: Trace a New Call

EASY

Using the variadic f and helper g from this article, predict the exact printed line for the call f(7, 'x', 3.5) and identify the call in which each value is printed.

Hints
  • Start with head = 7 and tail = 'x', 3.5.
  • Each call prints its head followed by a space, then calls f on the tail.
  • The empty call f() prints nothing, and main adds the newline.

Key Takeaways

  • A variadic template declares a parameter pack, typename... Args, that accepts zero or more arguments of types not fixed in advance.
  • The head-and-tail recursion handles one argument per call, passes the remaining arguments on, and stops at a required zero-argument base case.
  • The printed output follows the order in which the recursion reaches each value, and the helper's trailing space is part of the output.
  • Template argument deduction fills in parameters from function arguments; defaults fill in the rest, so a call like to(1.2) can omit the angle brackets.
  • When a result surprises you, check each template parameter to see whether it was written, deduced, or defaulted.

Related concepts

More concepts starting with T