Variadic templates
A variadic template takes any number of arguments of any types. The parameter pack collects them, sizeof... counts them, and a recursive call or a fold consumes them one at a time.
The standard library uses the form in std::make_unique, std::tuple and printf-style logging. This lesson writes the three pieces by hand: counting a pack, printing one element and recursing, and summing values of different arithmetic types.
A parameter pack and sizeof...
typename... Ts declares a pack of types, and Ts... values a matching pack of parameters. Inside the function sizeof...(Ts) is the number of types, known at compile time, and it works without looking at the values.
The program calls count with zero, one and three arguments. Each call instantiates a different function, and the answer is the argument count of that instantiation.
A pack can be empty. A function that only counts works for the empty call, which is not true of every variadic template: the next sections add a base case because their recursion needs one.
The arguments are taken by value in this example only to keep the signature short. A real function takes them by const reference, or forwards them, which is lesson 046.
#include <iostream>
template <typename... Ts>
std::size_t count(Ts...)
{
return sizeof...(Ts);
}
int main()
{
std::cout << count() << ' ' << count(1) << ' ' << count(1, 2.5, "three") << '\n';
}
g++ -std=c++23 -Wall -Wextra count.cpp -o count
./count
0 1 3
Printing a pack with recursion
The classic pattern has two functions: one that takes a single argument and stops, and one that takes at least one argument plus a pack, prints the first and calls itself with the rest. The pack shrinks on every call until only the base case is left.
The program prints four values of three types. The compiler generates one overload for each call in the chain, and the chain ends when the single-argument version is selected.
The base case must be a real overload, not a template that can also match zero arguments. That is what the last section is about.
The recursion is resolved entirely at compile time: there is no run-time loop and no container of arguments. Each call is a separate function with its own parameter types, which is also why a variadic template costs code size for every distinct call.
#include <iostream>
void print()
{
std::cout << '\n';
}
template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest)
{
std::cout << first << ' ';
print(rest...);
}
int main()
{
print(1, 2.5, "three", 'x');
}
g++ -std=c++23 -Wall -Wextra recursive.cpp -o recursive
./recursive
1 2.5 three x
Summing a pack
The same recursion adds values of different types. The sum starts as the first argument and the recursive call adds the rest, so the result type follows the usual arithmetic conversions of the additions.
The program sums three ints and then an int and two doubles. The two calls instantiate different functions, and the printed values show the int and the double result.
For arithmetic packs, C++17's fold expressions do the same job in one line; the next lesson rewrites this function with a fold and compares the two.
The recursion has one subtlety: the result type is the type of the first argument, so sum(1, 2.5) returns a double only because the first call adds 1 to the double result of the rest. A pack whose first element is an int and whose others are larger types can still lose precision on the first addition.
#include <iostream>
template <typename T>
T sum(const T& value)
{
return value;
}
template <typename T, typename... Rest>
T sum(const T& first, const Rest&... rest)
{
return first + sum(rest...);
}
int main()
{
std::cout << sum(1, 2, 3) << '\n';
std::cout << sum(1, 2.5, 3.5) << '\n';
}
g++ -std=c++23 -Wall -Wextra sum.cpp -o sum
./sum
6
7
A missing base case
The recursion needs a function to call when the pack is empty. The program below has only the variadic overload and no zero-argument version, so the final call print() has nothing to match.
The message says there is no matching function for the call with no arguments and lists the candidate it found, which requires at least one argument. The fix is the empty print() from the second section.
The same failure appears when the base case is a template that cannot be deduced for zero arguments. Writing an ordinary overload is the reliable way to end a recursive variadic template.
The instantiation trace in the message is long because the compiler shows every recursive step that led to the failing call. Read the last line first: it names the call with no arguments, which is the one the base case was supposed to handle.
#include <iostream>
template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest)
{
std::cout << first << ' ';
print(rest...);
}
int main()
{
print(1, 2, 3);
}
g++ -std=c++23 -Wall -Wextra no_base.cpp -o no_base
no_base.cpp: In instantiation of 'void print(const T&, const Rest& ...) [with T = int; Rest = {}]':
no_base.cpp:7:10: recursively required from 'void print(const T&, const Rest& ...) [with T = int; Rest = {int}]'
7 | print(rest...);
| ~~~~~^~~~~~~~~
no_base.cpp:7:10: required from 'void print(const T&, const Rest& ...) [with T = int; Rest = {int, int}]'
no_base.cpp:12:10: required from here
12 | print(1, 2, 3);
| ~~~~~^~~~~~~~~
no_base.cpp:7:10: error: no matching function for call to 'print()'
7 | print(rest...);
| ~~~~~^~~~~~~~~
no_base.cpp:7:10: note: there is 1 candidate
no_base.cpp:4:6: note: candidate 1: 'template<class T, class ... Rest> void print(const T&, const Rest& ...)'
4 | void print(const T& first, const Rest&... rest)
| ^~~~~
no_base.cpp:4:6: note: candidate expects at least 1 argument, 0 provided
Exercises
- Add a function that returns the largest of a pack and test it with ints and with doubles.
- Make print() in recursive.cpp separate its arguments with commas instead of spaces.
- Write a base case that prints a newline and check what the error in no_base.cpp becomes.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a parameter pack?
Answer
A template parameter that stands for zero or more arguments
The pack collects the arguments, and
...expands them where they are used. -
What does
sizeof...(args)give?Answer
The number of elements in the pack
The operator is evaluated at compile time and returns the count of the pack.
-
How is a pack consumed before C++17?
Answer
By a recursive call with one element and the rest
Each instantiation peels one element and passes the remainder to the next.
-
Why does the recursive form need a base case?
Answer
The empty pack still matches the template without one, so the recursion never stops
A non-variadic overload with no parameters ends the chain; without it the instantiation fails.
-
What does this program print?
#include <iostream> template <typename... Ts> std::size_t count(Ts...) { return sizeof...(Ts); } int main() { std::cout << count() << ' ' << count(1) << ' ' << count(1, 2.5, "three"); }Answer
0 1 3
sizeof... counts the pack, and the three calls have zero, one and three arguments.
-
What does this program print?
#include <iostream> void print() {} template <typename T, typename... Rest> void print(const T& first, const Rest&... rest) { std::cout << first << ' '; print(rest...); } int main() { print(1, 2.5, "three", 'x'); }Answer
1 2.5 three x
The recursion prints each element in order, and the empty overload ends the chain.
-
What does this program print?
#include <iostream> template <typename... Ts> auto sum(Ts... values) { return (0 + ... + values); } int main() { std::cout << sum(1, 2, 3) << ' ' << sum(1, 2, 1.5, 2.5); }Answer
6 7
The fold adds the integers and the mixed values, giving 6 and 7.
-
What does the missing-base-case error show?
Answer
An instantiation chain that ends in a call to print() with no arguments
The empty pack still tries to match the template, and the error lists the instantiations that led there.
-
How does C++17 fold expression print a pack?
Answer
(std::cout << ... << args)The fold applies the operator between every element, which is the theme of the next lesson.
-
Why is a variadic template useful for a logging function?
Answer
It accepts any number of values of any printable type
The caller writes a list of values, and the template formats each one with its own type.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev