The helpers we just implemented look horribly complicated. This is because we expand parameter packs with std::initializer_list. Why did we even use that data structure? Let's have a look at for_each again:
auto for_each ([](auto f, auto ...xs) {
(void)std::initializer_list<int>{
((void)f(xs), 0)...
};
});
The heart of this function is the f(xs) expression. xs is a parameter pack, and we need to unpack it in order to get the individual values out of it and feed them to individual f calls. Unfortunately, we cannot just write f(xs)... using the ... notation, which we already know.
What we can do is constructing a list of values using std::initializer_list, which has a variadic constructor. An expression such as return std::initializer_list<int>{f(xs)...}; does the job, but it has downsides. Let's have a look at an implementation of for_each which does just this, so it looks simpler than what we have:
auto for_each ([](auto f, auto ...xs) {
return std::initializer_list<int>{f(xs)...};
});
This is easier to grasp, but its downsides are the following:
- It constructs an actual initializer list of return values from all the f calls. At this point, we do not care about the return values.
- It returns that initializer list, although we want a "fire and forget" function, which returns nothing.
- It's possible that f is a function, which does not even return anything, in which case, this would not even compile.
The much more complicated for_each function fixes all these problems. It does the following things to achieve that:
- It does not return the initializer list, but it casts the whole expression to void using (void)std::initializer_list<int>{...}.
- Within the initializer expression, it wraps f(xs)... into an (f(xs), 0)... expression. This leads to the return value being thrown away, while 0 is put into the initializer list.
- The f(xs) in the (f(xs), 0)... expression is again cast to void, so the return value is really not processed anywhere if it has any.
Putting all this together unluckily leads to an ugly construct, but it does it's work right and compiles with a whole variety of function objects, regardless of whether they return anything or what they return.
A nice detail of this technique is that the order in which the function calls are applied is guaranteed to be in a strict sequence.