In this section, we will write a program that can print any tuple on the fly. In addition to that, we will write a function that can zip tuples together:
- We need to include a number of headers first and then we declare that we use the std namespace by default:
#include <iostream>
#include <tuple>
#include <list>
#include <utility>
#include <string>
#include <iterator>
#include <numeric>
#include <algorithm>
using namespace std;
- As we will be dealing with tuples, it will be interesting to display their content. Therefore, we will now implement a very generic function that can print any tuple that consists of printable types. The function accepts an output stream reference os, which will be used to do the actual printing, and a variadic argument list, which carries all the tuple members. We decompose all the arguments into the first element and put it into the argument, v, and the rest, which is stored in the argument pack vs...:
template <typename T, typename ... Ts>
void print_args(ostream &os, const T &v, const Ts &...vs)
{
os << v;
- If there are arguments left in the parameter pack, vs, these are printed interleaved with ", " using the initializer_list expansion trick. You learned about this trick in the Chapter 21, Lambda Expressions:
(void)initializer_list<int>{((os << ", " << vs), 0)...};
}
- We can now print arbitrary sets of arguments by writing print_args(cout, 1, 2, "foo", 3, "bar"), for example. But this has nothing to do with tuples yet. In order to print tuples, we overload the stream output operator << for any case of tuples by implementing a template function that matches on any tuple specialization:
template <typename ... Ts>
ostream& operator<<(ostream &os, const tuple<Ts...> &t)
{
- Now it gets a little complicated. We first use a lambda expression that arbitrarily accepts many parameters. Whenever it is called, it prepends the os argument to those arguments and then calls print_args with the resulting new list of arguments. This means that a call to capt_tup(...some parameters...) leads to a print_args(os, ...some parameters...) call:
auto print_to_os ([&os](const auto &...xs) {
print_args(os, xs...);
});
- Now we can do the actual tuple unpacking magic. We use std::apply to unpack the tuple. All values will be taken out of the tuple then and lined up as function arguments for the function that we provide as the first argument. This just means that if we have a tuple, t = (1, 2, 3), and call apply(capt_tup, t), then this will lead to a function call, capt_tup(1, 2, 3), which in turn leads to the function call, print_args(os, 1, 2, 3). This is just what we need. As a nice extra, we surround the printing with parentheses:
os << "(";
apply(print_to_os, t);
return os << ")";
}
- Okay, now we wrote some complicated code that will make our life much easier when we want to print a tuple. But we can do a lot more with tuples. Let's, for example, write a function that accepts an iterable range, such as a vector or a list of numbers, as an argument. This function will then iterate over that range and then return us the sum of all the numbers in the range and bundle that with the minimum of all values, the maximum of all values, and the numeric average of them. By packing these four values into a tuple, we can return them as a single object without defining an additional structure type:
template <typename T>
tuple<double, double, double, double>
sum_min_max_avg(const T &range)
{
- The std::minmax_element function returns us a pair of iterators that respectively point to the minimum and maximum values of the input range. The std::accumulate method sums up all the values in its input range. This is all we need to return the four values that fit in our tuple!
auto min_max (minmax_element(begin(range), end(range)));
auto sum (accumulate(begin(range), end(range), 0.0));
return {sum, *min_max.first, *min_max.second,
sum / range.size()};
}
- Before implementing the main program, we will implement one last magic helper function. I call it magic because it really looks complicated at first, but after understanding how it works, it will turn out as a really slick and nice helper. It will zip two tuples. This means that if we feed it a tuple, (1, 2, 3), and another tuple, ('a', 'b', 'c'), it will return a tuple (1, 'a', 2, 'b', 3, 'c'):
template <typename T1, typename T2>
static auto zip(const T1 &a, const T2 &b)
{
- Now we arrived at the most complex lines of code of this recipe. We create a function object, z, which accepts an arbitrary number of arguments. It then returns another function object that captures all these arguments in a parameter pack, xs, but also accepts another arbitrary number of arguments. Let's sink this in for a moment. Within this inner function object, we can access both lists of arguments in the form of the parameter packs, xs and ys. And now let's have a look what we actually do with these parameter packs. The expression, make_tuple(xs, ys)..., groups the parameter packs item wise. This means that if we have xs = 1, 2, 3 and ys = 'a', 'b', 'c', this will result in a new parameter pack, (1, 'a'), (2, 'b'), (3, 'c'). This is a comma-separated list of three tuples. In order to get them all grouped in one tuple, we use std::tuple_cat, which accepts an arbitrary number of tuples and repacks them into one tuple. This way we get a nice (1, 'a', 2, 'b', 3, 'c') tuple:
auto z ([](auto ...xs) {
return [xs...](auto ...ys) {
return tuple_cat(make_tuple(xs, ys) ...);
};
});
- The last step is unwrapping all the values from the input tuples, a and b, and pushing them into z. The expression, apply(z, a), puts all the values from a into the parameter pack xs, and apply(..., b) puts all the values of b into the parameter pack ys. The resulting tuple is the large zipped one, which we return to the caller:
return apply(apply(z, a), b);
}
- We invested a considerable amount of lines into helper/library code. Let's now finally put it to use. First, we construct some arbitrary tuples. The student contains ID, name, and GPA score of a student. The student_desc contains strings that describe what those fields mean in human-readable form. The std::make_tuple is a really nice helper because it automatically deduces the type of all the arguments and creates a suitable tuple type:
int main()
{
auto student_desc (make_tuple("ID", "Name", "GPA"));
auto student (make_tuple(123456, "John Doe", 3.7));
- Let's just print what we have. This is really simple because we just implemented the right operator<< overload for that:
cout << student_desc << 'n'
<< student << 'n';
- We can also group both the tuples on the fly with std::tuple_cat and print them like this:
cout << tuple_cat(student_desc, student) << 'n';
- We can also create a new zipped tuple with our zip function and also print it:
auto zipped (zip(student_desc, student));
cout << zipped << 'n';
- Let's not forget our sum_min_max_avg function. We create an initializer list that contains some numbers and feed it into this function. To make it a little bit more complicated, we create another tuple of the same size, which contains some describing strings. By zipping these tuples, we get a nice, interleaved output, as we will see when we run the program:
auto numbers = {0.0, 1.0, 2.0, 3.0, 4.0};
cout << zip(
make_tuple("Sum", "Minimum", "Maximum", "Average"),
sum_min_max_avg(numbers))
<< 'n';
}
- Compiling and running the program yields the following output. The first two lines are just the individual student and student_desc tuples. Line 3 is the tuple composition we got by using tuple_cat. Line 4 contains the zipped student tuple. In the last line, we see the sum, minimum, maximum, and average value of the numeric list we last created. Because of the zipping, it is really easy to see what each value means:
$ ./tuple
(ID, Name, GPA)
(123456, John Doe, 3.7)
(ID, Name, GPA, 123456, John Doe, 3.7)
(ID, 123456, Name, John Doe, GPA, 3.7)
(Sum, 10, Minimum, 0, Maximum, 4, Average, 2)