Before we even touched operator<< for output streams, we implemented the print_args function. Due to its variadic argument nature, it accepts any number and type of arguments, as long as the first one is an ostream instance:
template <typename T, typename ... Ts>
void print_args(ostream &os, const T &v, const Ts &...vs)
{
os << v;
(void)initializer_list<int>{((os << ", " << vs), 0)...};
}
This function prints the first item, v, and then prints all the other items from the parameter pack, vs. We print the first item individually because we want to have all items interleaved with ", " but we do not want this string leading or trailing the whole list (as in "1, 2, 3, " or ", 1, 2, 3"). We learned about the initializer_list expansion trick in Chapter 21, Lambda Expressions, in the recipe Calling multiple functions with the same input.
Having that function lined up, we have everything we need in order to print tuples. Our operator<< implementation looks as follows:
template <typename ... Ts>
ostream& operator<<(ostream &os, const tuple<Ts...> &t)
{
auto capt_tup ([&os](const auto &...xs) {
print_args(os, xs...);
});
os << "(";
apply(capt_tup, t);
return os << ")";
}
The first thing we do is defining the function object, capt_tup. When we call capt_tup(foo, bar, whatever), this results in the call, print_args(os, foo, bar, whatever). The only thing this function object does is prepend the output stream object os to its variadic list of arguments.
Afterward, we use std::apply in order to unpack all the items from tuple t. If this step looks too complicated, please have a look at the recipe before this one, which is dedicated to demonstrating how std::apply works.