Variadic templates
Functions and types that take any number of arguments.
By the end of this chapter you can
- Write a variadic function template with a fold expression
- Explain how a parameter pack is expanded
- Implement a simple type-safe printf
C’s answer to “any number of arguments” is printf, and the price is that the
compiler cannot check it. Pass an int where the format string promised
%s and you get undefined behaviour, not a diagnostic.
C++ can do better because a variadic template knows every argument’s type. The count and the types are fixed at the call site, the same as for any other template — there is just no fixed number of them.
#include <iostream>
#include <string>
template <class... Args>
void print(const Args&... args) {
((std::cout << args << ' '), ...);
std::cout << '\n';
}
int main() {
print(1, 2.5, "three", std::string{"four"}, 'x', true);
print("just one");
print();
}Six arguments of six different types, one function, no format string to get
wrong. Add a type with no operator<< and the error names that type at your
call.
A pack is not a type
class... Args declares a template parameter pack; const Args&... args
declares a function parameter pack. Neither is a thing you can pass around
on its own. args is not a container, has no begin(), and cannot be indexed.
The only two things you can do with a pack are ask how big it is and expand
it.
#include <iostream>
template <class... Args>
void describe(const Args&... args) {
std::cout << sizeof...(Args) << " arguments: ";
((std::cout << '[' << args << ']'), ...);
std::cout << '\n';
}
int main() {
describe(1, 'b', 3.5);
describe();
}sizeof...(Args) and sizeof...(args) both give the count — a std::size_t,
known at compile time, usable in a static_assert.
Expansion is written as a pattern followed by .... The pattern is repeated
once per element, with the pack name replaced each time, and the copies are
separated by commas:
| You write | It becomes |
|---|---|
args... |
a1, a2, a3 |
&args... |
&a1, &a2, &a3 |
f(args)... |
f(a1), f(a2), f(a3) |
std::forward<Args>(args)... |
std::forward<A1>(a1), … |
static_cast<long>(args)... |
static_cast<long>(a1), … |
The ... applies to the whole pattern, not to the nearest name — which is why
f(args)... calls f three times rather than calling it once with three
arguments.
Fold expressions
A comma-separated list is only useful where a list is expected. To combine
the elements with an operator, you need a fold expression (C++17): a
parenthesised pack, an operator, and ....
#include <iostream>
template <class... Args> auto right_fold(Args... a) { return (a - ...); }
template <class... Args> auto left_fold(Args... a) { return (... - a); }
template <class... Args> auto right_init(Args... a) { return (a - ... - 100); }
template <class... Args> auto left_init(Args... a) { return (100 - ... - a); }
int main() {
std::cout << "(a - ...) = " << right_fold(1, 2, 3) << " // 1 - (2 - 3)\n";
std::cout << "(... - a) = " << left_fold(1, 2, 3) << " // (1 - 2) - 3\n";
std::cout << "(a - ... - 100)= " << right_init(1, 2, 3) << " // 1 - (2 - (3 - 100))\n";
std::cout << "(100 - ... - a)= " << left_init(1, 2, 3) << " // ((100 - 1) - 2) - 3\n";
}The mnemonic: the ... sits where the rest of the fold goes. (a - ...)
puts the remainder on the right, so it nests to the right. For associative
operators like + the direction does not change the answer; for -, /, and
<< it does, and left folds are usually what you want because they match how
the operator reads.
Almost any binary operator works, including ,, &&, ||, <<, and =.
#include <iostream>
template <class... Args> auto total(const Args&... args) { return (0 + ... + args); }
template <class... Args> bool all_positive(const Args&... args) { return (... && (args > 0)); }
int main() {
std::cout << total(1, 2, 3) << ' ' << total() << '\n';
std::cout << std::boolalpha << all_positive(1, 2, 3) << ' '
<< all_positive(1, -2) << ' ' << all_positive() << '\n';
}Note (args > 0) in parentheses. The pattern of a fold must be a single
operand, and args > 0 && ... would parse as the wrong thing. Parenthesise
anything that is not a bare name.
Evaluation order: the comma fold earns its keep
Fold expressions over &&, ||, and , guarantee left-to-right evaluation.
Function arguments do not — the order in which a call’s arguments are evaluated
is unspecified, and compilers differ.
#include <iostream>
int next_id() { static int n = 0; return ++n; }
template <class... Args>
void as_call(Args... args) { ((std::cout << args << ' '), ...); std::cout << '\n'; }
int main() {
std::cout << "expanded as call arguments: ";
as_call(next_id(), next_id(), next_id());
std::cout << "expanded as a comma fold: ";
((std::cout << next_id() << ' '), (std::cout << next_id() << ' '),
(std::cout << next_id() << ' '));
std::cout << '\n';
}On GCC the first line prints 3 2 1 — the arguments were evaluated
right-to-left before as_call ever ran. The second line prints 4 5 6,
because a comma fold is ordered. Another compiler may print 1 2 3 for the
first line and still be conforming.
So: when the elements have side effects and the order matters, put the side effect inside a comma fold, not in the argument list of a call.
A type-safe printf
That is enough machinery to replace the thing we started with.
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
template <class... Args>
std::string format(std::string_view pattern, const Args&... args) {
std::ostringstream out;
std::size_t pos = 0;
[[maybe_unused]] auto emit = [&](const auto& value) { // unused when Args is empty
std::size_t brace = pattern.find("{}", pos);
if (brace == std::string_view::npos) return; // more args than slots
out << pattern.substr(pos, brace - pos) << value;
pos = brace + 2;
};
(emit(args), ...); // ordered, left to right
out << pattern.substr(pos);
return out.str();
}
int main() {
std::cout << format("{} scored {} out of {}\n", "ada", 91.5, 100);
std::cout << format("no placeholders here\n");
std::cout << format("{} and {}\n", 1, 2, 3); // extra argument: ignored
}Three things this gets for free that printf cannot. Every argument is printed
by the operator<< chosen for its type, so there is no format specifier to
mismatch. A std::string works without .c_str(). And the whole thing is
ordinary code — the emit lambda is called once per argument, in order,
because the comma fold says so.
Packs expand in more places than you think
Pack expansion is not limited to function arguments. It works in template
argument lists, in base-class lists, in using-declarations, and in braced
initialiser lists. The classic demonstration is the overloaded idiom: build
one callable out of several lambdas by inheriting from all of them.
#include <iostream>
#include <string>
#include <variant>
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...; // pull every base's operator() into scope
};
int main() {
std::variant<int, double, std::string> values[] = {42, 2.5, std::string{"text"}};
auto describe = overloaded{
[](int i) { std::cout << "int " << i << '\n'; },
[](double d) { std::cout << "double " << d << '\n'; },
[](const std::string& s) { std::cout << "string \"" << s << "\"\n"; },
};
for (const auto& v : values) std::visit(describe, v);
}Two expansions, both unusual. struct overloaded : Ts... expands the pack into
the base-class list, so the struct derives from all three lambda types.
using Ts::operator()... expands into three using-declarations, which is what
makes overload resolution see all three call operators as one overload set —
without it, the inherited names would hide each other and the call would be
ambiguous.
When a fold is not enough
Folds handle “do the same thing to every element”. When each element needs to know its position, or the elements must be handled pairwise, you fall back to recursion — a base case and a step that peels one argument off the front.
#include <iostream>
#include <string_view>
void print_numbered(int, std::string_view) {} // end of the pack
template <class First, class... Rest>
void print_numbered(int index, std::string_view label, const First& first, const Rest&... rest) {
std::cout << label << ' ' << index << ": " << first << '\n';
if constexpr (sizeof...(rest) > 0)
print_numbered(index + 1, label, rest...);
}
int main() {
print_numbered(1, "row", "alpha", 2, 3.5, 'z');
}if constexpr is doing real work here: without it, the compiler would still
have to compile the recursive call for the empty case, and the overload that
takes no pack would have to exist for a reason other than never being called.
With it, the branch disappears when the pack is empty.
Before C++17 this was the only way to write any of this, which is why older generic code is full of head/tail recursion. If a fold expresses what you mean, use the fold: it compiles faster and reads better.