Lesson 6 of 8
std::format("{} is {}", name, age) takes three arguments. std::format("{}", x) takes two. One
template handles both, and any other count.
That is a variadic template, and the mechanism is a parameter pack.
The syntax
template <typename... Args>
void print_all(const Args&... args);
Three uses of ..., each meaning something different:
typename... Args— declaresArgsas a pack of types, zero or more.const Args&... args— declaresargsas a pack of parameters, one per type.args...(at a call site) — expands the pack back into a comma-separated list.
The first two pack; the third unpacks.
Folding over the pack
#include <iostream>
#include <string>
// Args is a parameter pack: zero or more types.
template <typename... Args>
void print_all(const Args&... args) {
// Fold expression (C++17): expands to arg1 << " " << arg2 << " " << ...
((std::cout << args << " "), ...);
std::cout << "\n";
}
// sizeof...(pack) gives the number of elements, at compile time.
template <typename... Args>
void report(const Args&... args) {
std::cout << "got " << sizeof...(args) << " arguments: ";
print_all(args...);
}
int main() {
print_all(1, 2, 3);
print_all("mixed", 42, 3.5, 'x');
print_all();
report(1, 2, 3);
report(std::string("one"));
return 0;
}
1 2 3
mixed 42 3.5 x
got 3 arguments: 1 2 3
got 1 arguments: one
Note the blank line: print_all() with an empty pack is valid, and prints only the newline.
sizeof...(args) is the pack's length, known at compile time — not to be confused with
sizeof(x), which is a size in bytes.
Fold expressions
((std::cout << args << " "), ...) is a fold expression, added in C++17. It applies an
operator across the whole pack.
There are four forms, and the difference between them matters:
#include <iostream>
#include <string>
// The recursive form, which is how this was done before C++17.
// Base case: no arguments left.
void print_recursive() {
std::cout << "\n";
}
// Peel one argument off the front, recurse on the rest.
template <typename First, typename... Rest>
void print_recursive(const First& first, const Rest&... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print_recursive(rest...);
}
// The four fold forms.
template <typename... Args>
auto sum_right(Args... args) { return (args + ...); } // a + (b + (c + d))
template <typename... Args>
auto sum_left(Args... args) { return (... + args); } // ((a + b) + c) + d
template <typename... Args>
auto sum_with_zero(Args... args) { return (args + ... + 0); } // works for an empty pack
template <typename... Args>
bool all_true(Args... args) { return (args && ...); }
int main() {
print_recursive(1, 2, 3);
print_recursive("a", 'b', 3.5);
std::cout << sum_right(1, 2, 3, 4) << "\n";
std::cout << sum_left(1, 2, 3, 4) << "\n";
std::cout << sum_with_zero() << "\n";
std::cout << std::boolalpha << all_true(true, true, false) << "\n";
// Left and right differ for non-associative operations.
std::cout << "right: " << (1.0 / (2.0 / 4.0)) << "\n";
return 0;
}
1, 2, 3
a, b, 3.5
10
10
0
false
right: 2
| Form | Name | Expands to |
|---|---|---|
| (args + ...) | unary right fold | a + (b + (c + d)) |
| (... + args) | unary left fold | ((a + b) + c) + d |
| (args + ... + init) | binary right fold | a + (b + (c + init)) |
| (init + ... + args) | binary left fold | ((init + a) + b) + c |
sum_right and sum_left both print 10 because addition is associative. For subtraction or
division they differ, which the last line illustrates: 1/(2/4) is 2, while ((1/2)/4) is 0.125.
Prefer the left fold, since it matches how the operator normally groups.
The binary forms exist for the empty pack. sum_right() with no arguments would not compile —
there is nothing to add — while sum_with_zero() returns the initial value, which is why it
prints 0.
Recursion, the older way
print_recursive above shows how this was done before fold expressions, and you will still meet
it because folds cannot express everything.
The pattern: peel the first argument off, handle it, recurse on the rest. Termination is a non-template overload taking no arguments.
if constexpr (sizeof...(rest) > 0) is a compile-time if (C++17): the branch not taken is not
compiled at all. That is what stops a comma being printed after the last element. An ordinary if
would work here too, but if constexpr is essential when the untaken branch would not compile
for some instantiation — which is common in variadic code.
Perfect forwarding, and what it is for
The most important use of parameter packs is passing arguments through to a constructor without losing anything:
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct Widget {
std::string name;
int size;
Widget(std::string n, int s) : name(std::move(n)), size(s) {
std::cout << "Widget(" << name << ", " << size << ") constructed\n";
}
};
// Perfect forwarding: take anything, pass it on unchanged.
template <typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main() {
auto w = make<Widget>("gear", 5);
std::cout << w->name << " " << w->size << "\n";
// emplace_back is the same idea: it forwards to the element's constructor,
// so the object is built in place rather than built and then moved.
std::vector<Widget> widgets;
widgets.reserve(2);
widgets.emplace_back("bolt", 1);
widgets.emplace_back("nut", 2);
std::cout << widgets.size() << " widgets\n";
return 0;
}
Widget(gear, 5) constructed
gear 5
Widget(bolt, 1) constructed
Widget(nut, 2) constructed
2 widgets
That make function is std::make_unique, near enough. emplace_back is the same mechanism
applied to a container.
Two pieces do the work:
Args&&... args is a forwarding reference, not an rvalue reference. In a deduced context
T&& binds to anything — lvalue or rvalue — and remembers which it was in T. The identical
syntax means an rvalue reference only when the type is not deduced, which is a real source of
confusion (lesson 9.9).
std::forward<Args>(args)... restores what the caller passed. Without it, everything inside
the function is a named variable and therefore an lvalue, so an rvalue argument would be copied
rather than moved.
Why forwarding is worth the syntax
Measured with a type that reports its own copies and moves:
#include <iostream>
#include <string>
#include <utility>
struct Tracker {
std::string label;
explicit Tracker(std::string l) : label(std::move(l)) {}
Tracker(const Tracker& other) : label(other.label) {
std::cout << " copy of " << label << "\n";
}
Tracker(Tracker&& other) noexcept : label(std::move(other.label)) {
std::cout << " move of " << label << "\n";
}
};
void consume(Tracker t) { std::cout << " consumed " << t.label << "\n"; }
// Without forwarding: everything arrives as an lvalue inside, so it copies.
template <typename T>
void relay_by_value(T arg) { consume(arg); }
// With forwarding: T&& plus std::forward preserves lvalue-ness or rvalue-ness.
template <typename T>
void relay_forward(T&& arg) { consume(std::forward<T>(arg)); }
int main() {
Tracker a("named");
std::cout << "relay_by_value(a):\n";
relay_by_value(a);
std::cout << "relay_forward(a):\n";
relay_forward(a);
std::cout << "relay_forward(temporary):\n";
relay_forward(Tracker("temp"));
return 0;
}
relay_by_value(a):
copy of named
copy of named
consumed named
relay_forward(a):
copy of named
consumed named
relay_forward(temporary):
move of temp
consumed temp
Count them. Passing by value costs two copies — one into the relay, one into consume.
Forwarding an lvalue costs one, the unavoidable one. Forwarding a temporary costs zero
copies: it moves.
For a Tracker holding one string that is a small saving. For a std::vector of a million
elements it is the difference between a memcpy and a pointer swap.
This is exactly why emplace_back(args...) can beat push_back(T(args...)): the latter builds a
temporary and moves it, the former builds the element in place.
Variadic class templates
Packs work on classes too. std::tuple is the canonical example:
// template <typename... Types>
// class tuple;
//
// std::tuple<int, std::string, double> row{1, "name", 2.5};
//
// So is std::variant (lesson 13.10):
//
// template <typename... Types>
// class variant;
//
// std::variant<int, std::string> value = 42;
Writing one is harder than writing a variadic function, because storing a pack requires recursive
inheritance or a recursive member. In practice you use std::tuple rather than writing your own.
Try it
Write a variadic log(level, args...) that prints the level then every argument separated by
spaces. Add count_matching<T>(args...) returning how many arguments are of type T. Then
write sum_all with a fold and make it work correctly with no arguments.
Checkpoint
typename... Argsdeclares a parameter pack;args...expands one.sizeof...(args)is the pack's length at compile time.- Fold expressions (C++17) apply an operator across a pack in one line.
- Prefer the left fold
(... + args); use the binary form to handle an empty pack. - The older recursive form still applies when a fold cannot express the logic.
if constexprdiscards the untaken branch entirely, which variadic code often needs.Args&&...in a deduced context is a forwarding reference, not an rvalue reference.std::forward<Args>(args)...preserves lvalue/rvalue-ness — without it everything copies.- Perfect forwarding is what makes
std::make_uniqueandemplace_backavoid copies. std::tupleandstd::variantare variadic class templates.
Questions about this lesson
Log in to ask a question.