Writing a function template that accepts exactly two arguments is straightforward, but useful operations often take three, or four, or many. It’s unfeasible to hand write an overload for each number of possible arguments. Variadic templates provide an answer; one template can accommodate an arbitrary number of arguments.
A straightforward version for two arguments is perfectly understandable.
#include <iostream>
// Make the following definition depend on template arguments
template <typename T, typename U>
void printBoth(T first, U second)
{
std::cout << first << ' ' << second << '\n';
}
int main()
{
printBoth(10, 3.5);
return 0;
}
The types T and U in the template parameters represent the types of the two arguments. The caller provides an int and a double; a specific function that prints those two values separated by a space will be generated. But there is no place for a third argument.
A variadic template frees up any such restriction. A type parameter that has an ellipsis after its name is a parameter pack; it’s a parameter of open ended arity (zero or more arguments of given types).
#include <iostream>
// Accept a parameter pack with any number of template arguments
template <typename... Args>
void printAll(Args... args)
{
(std::cout << ... << args) << '\n';
}
int main()
{
printAll(10, 3.5, 'X');
return 0;
}
We name the pack of types Args in the template head template <typename... Args>. The corresponding function parameter pack parameter is Args... args; an in context usage such as in a function’s parameter list will be expanded into as many actual parameters as there are types in Args.
In the function’s body, the (std::cout << ... << args) expression is a parameter pack expansion, a feature that folds the elements of the pack.
Here, it prints each element separated by std::cout (which implicitly places a space before each subsequent element but not the first), followed by '\n'.
In an invocation printAll(10, 3.5, 'X'), the compiler looks at the three arguments and synthesizes a parameter pack Args consisting of the types int, double, char. It then instantiates a concrete function signature of a print function with just these types. A simple expansion turns it into std::cout << 10 << ' ' << 3.5 << ' ' << 'X' << '\n', then that is converted to std::cout << 10 << ' ' << 3.5 << ' ' << 'X' << '\n'.
An invocation with a different number of arguments results in a differently specialized function.
#include <iostream>
template <typename... Args>
void printAll(Args... args)
{
(std::cout << ... << args) << '\n';
}
int main()
{
printAll(7);
printAll(1, 2, 3, 4);
return 0;
}
A single argument results in an Args pack of size 1 with type int, so the generated function simply prints the number and a newline. Four integer arguments create an Args pack of size 4. Each sequence of argument types uniquely determines the instantiation of the function to call.
An invocation with zero arguments is also valid:
#include <iostream>
template <typename... Args>
void printAll(Args... args)
{
(std::cout << ... << args) << '\n';
}
int main()
{
printAll();
return 0;
}
Here, the pack is empty. The fold over an empty pack evaluates to a trivial value for the binary operator <<, and the line simply becomes std::cout << '\n', printing a blank line. This follows as an inherent consequence of the pack's zero or more arity.
When you want to process elements one by one instead of all at once in a fold, recursion is a common tool. For this, we'll need a base case function template specializing for a single argument, as the recursion termination condition.
#include <iostream>
template <typename T>
void printAll(T value)
{
std::cout << value << '\n';
}
template <typename T, typename... Rest>
void printAll(T first, Rest... rest)
{
std::cout << first << ' ';
printAll(rest...);
}
int main()
{
printAll(5, 6.2, 'A');
return 0;
}
The first template argument (no ...) forms the base case. The second one forms the recursive step: it consumes the first argument and packs up the remaining ones with Rest....
The function body prints the first argument and then recursively invokes itself with the pack expanded into arguments to the remaining ones.
Given the invocation printAll(5, 6.2, 'A'), the recursion unwinds as follows:
printAll(5, 6.2, 'A')matches the recursive template, prints 5, and recurses withprintAll(6.2, 'A').printAll(6.2, 'A')matches the recursive template, prints 6.2, and recurses withprintAll('A').printAll('A')matches the base case template, printsA, and prints the newline\n.
The end result is all arguments printed, separated by spaces, on a single line.
Be mindful that you must use the ellipsis (...) where the expanded pack is needed. A bare rest argument without the ellipsis would be a compile time error, because there is no such single entity as the rest parameter pack itself; it's simply shorthand for a sequence of zero or more items.
Exercise: Variadic TemplatesStarter code, hints and solutionOpenClose
Practice: Variadic Templates
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Add printAll(std::string{"Hi"}, true) with the needed header. Trace how the parameter pack shrinks and which overload ends the recursion.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
template <typename T>
void printAll(T value)
{
std::cout << value << '\n';
}
template <typename T, typename... Rest>
void printAll(T first, Rest... rest)
{
std::cout << first << ' ';
printAll(rest...);
}
int main()
{
printAll(5, 6.2, 'A');
return 0;
}Hints
The variadic overload prints first, then expands rest... into a smaller call. The one-argument overload is the stopping case.
Add another argument and count how many times the pack shrinks. Do not end with printAll() unless you also define a zero-argument overload.
Worked solution
The additional printAll call introduces std::string and bool arguments. The variadic overload prints Hi and recurses with the remaining bool; the one-argument overload ends the recursion and prints 1 followed by a newline.
#include <string>
#include <iostream>
template <typename T>
void printAll(T value)
{
std::cout << value << '\n';
}
template <typename T, typename... Rest>
void printAll(T first, Rest... rest)
{
std::cout << first << ' ';
printAll(rest...);
}
int main()
{
printAll(5, 6.2, 'A');
printAll(std::string{"Hi"}, true);
return 0;
}
Example output
5 6.2 A
Hi 1
0 comments
No comments yet. Be the first.