Anatomy of pretty print tuple
Asked Answered
S

2

5

A while ago, a solution to print out std::tuple was posted here. For the most part I get what's happening. I'm having trouble understanding whats happening in the print_tuple function though.

template<class Ch, class Tr, class Tuple, std::size_t... Is>
void print_tuple(std::basic_ostream<Ch,Tr>& os, Tuple const& t, seq<Is...>){
  using swallow = int[];
  (void)swallow{0, (void(os << (Is == 0? "" : ", ") << std::get<Is>(t)), 0)...};
}

I don't get what's happening in the body of this function. As far as I can tell, it has something to do with unpacking Is. I get that the condition, Is == 0 is checking to see if we're at the head element.

So what's going on?

Stinkwood answered 2/12, 2014 at 15:16 Comment(2)
The code constructs (and then throws away) an int[] array from an initializer list, where each element is 0 but prints one element of the tuple as a side effect (via comma operator). The use of initializer list is just to get into a context where pack expansion would work.Caesar
Ah! So the swallow{...} construct is an initialization list for int[]. I did not catch that at first glance.Stinkwood
H
8

Let's go through an example with an arbitrary tuple, say:

using Tuple = tuple<char, int, string>;

Thus, the integer sequence that our function will be called with is:

seq<0, 1, 2>

And our pack expansion in the body is:

(void)swallow{0, (void(os << (Is == 0? "" : ", ") << std::get<Is>(t)), 0)...};

which, if we manually expand it the way the compiler would, becomes:

(void)swallow{0,
              (void(os << (0 == 0? "" : ", ") << std::get<0>(t)), 0),
              (void(os << (1 == 0? "" : ", ") << std::get<1>(t)), 0),
              (void(os << (2 == 0? "" : ", ") << std::get<2>(t)), 0)
              };

And then evaluating the branches:

(void)swallow{0,
              (void(os << "" << std::get<0>(t)), 0),
              (void(os << ", " << std::get<1>(t)), 0),
              (void(os << ", " << std::get<2>(t)), 0)
              };

Which is to say, we're constructing an integer array of 4 0s, with the side effect of printing out the contents of the tuple, comma-separated, making sure we don't start with an extra comma. The four expressions must be evaluated in order, so that guarantees that the tuple contents get printed in order.

The initial (void) cast is just there to avoid the unused-variable warning that compilers should emit if you have all warnings turned on. The initial 0 in the array initialization handles the case where the tuple is empty.

Hatred answered 2/12, 2014 at 15:25 Comment(4)
I'm starting to get it. What does the void(...) do?Stinkwood
@SalvadorGuzman Casts the result of os << whatever to void, so that the expression (void(stuff), 0) definitely returns 0. This is done on the off chance that somebody decided to write an overload for operator,() that does something other than return the int.Hatred
This is making much more sense. One last question. So is the '...' operator an unpacker? Can I count on it to return comma separated values?Stinkwood
@SalvadorGuzman It's not an operator, but it does expand the parameter pack. Read through this reference, it's a good intro.Hatred
R
0

(os << (Is == 0? "" : ", ") << std::get<Is>(t)) print the Isth element (with prefix ", " for Is > 0)

Then, casting the result to void to avoid possible overload of comma operator.

(/*previous stuff*/, 0)... does a series of 0

{0, /*previous stuff*/ } manages the cases where sizeof...(Is) == 0

swallow /*previous stuff*/ build an int array of 0.

void /*previous stuff*/ : cast to void to avoid warning.

Roberson answered 2/12, 2014 at 15:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.