This is a follow-up to my previous question on pretty-printing STL containers, for which we managed to develop a very elegant and fully general solution.
In this next step, I would like to include pretty-printing for std::tuple<Args...>
, using variadic templates (so this is strictly C++11). For std::pair<S,T>
, I simply say
std::ostream & operator<<(std::ostream & o, const std::pair<S,T> & p) { return o << "(" << p.first << ", " << p.second << ")"; }
What is the analogous construction for printing a tuple?
I've tried various bits of template argument stack unpacking, passing indices around and using SFINAE to discover when I'm at the last element, but with no success. I shan't burden you with my broken code; the problem description is hopefully straight-forward enough. Essentially, I'd like the following behaviour:
auto a = std::make_tuple(5, "Hello", -0.1); std::cout << a << std::endl; // prints: (5, "Hello", -0.1)
Bonus points for including the same level of generality (char/wchar_t, pair delimiters) as the the previous question!
Yay, indices~
namespace aux{ template<std::size_t...> struct seq{}; template<std::size_t N, std::size_t... Is> struct gen_seq : gen_seq<N-1, N-1, Is...>{}; template<std::size_t... Is> struct gen_seq<0, Is...> : seq<Is...>{}; 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)...}; } } // aux:: template<class Ch, class Tr, class... Args> auto operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) -> std::basic_ostream<Ch, Tr>& { os << "("; aux::print_tuple(os, t, aux::gen_seq<sizeof...(Args)>()); return os << ")"; }
Live example on Ideone.
For the delimiter stuff, just add these partial specializations:
// Delimiters for tuple template<class... Args> struct delimiters<std::tuple<Args...>, char> { static const delimiters_values<char> values; }; template<class... Args> const delimiters_values<char> delimiters<std::tuple<Args...>, char>::values = { "(", ", ", ")" }; template<class... Args> struct delimiters<std::tuple<Args...>, wchar_t> { static const delimiters_values<wchar_t> values; }; template<class... Args> const delimiters_values<wchar_t> delimiters<std::tuple<Args...>, wchar_t>::values = { L"(", L", ", L")" };
and change the operator<<
and print_tuple
accordingly:
template<class Ch, class Tr, class... Args> auto operator<<(std::basic_ostream<Ch, Tr>& os, std::tuple<Args...> const& t) -> std::basic_ostream<Ch, Tr>& { typedef std::tuple<Args...> tuple_t; if(delimiters<tuple_t, Ch>::values.prefix != 0) os << delimiters<tuple_t,char>::values.prefix; print_tuple(os, t, aux::gen_seq<sizeof...(Args)>()); if(delimiters<tuple_t, Ch>::values.postfix != 0) os << delimiters<tuple_t,char>::values.postfix; return os; }
And
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[]; char const* delim = delimiters<Tuple, Ch>::values.delimiter; if(!delim) delim = ""; (void)swallow{0, (void(os << (Is == 0? "" : delim) << std::get<Is>(t)), 0)...}; }
In C++17 we can accomplish this with a little less code by taking advantage of Fold expressions, particularly a unary left fold:
template<class TupType, size_t... I> void print(const TupType& _tup, std::index_sequence<I...>) { std::cout << "("; (..., (std::cout << (I == 0? "" : ", ") << std::get<I>(_tup))); std::cout << ")\n"; } template<class... T> void print (const std::tuple<T...>& _tup) { print(_tup, std::make_index_sequence<sizeof...(T)>()); }
Live Demo outputs:
(5, Hello, -0.1)
given
auto a = std::make_tuple(5, "Hello", -0.1); print(a);
Our unary left fold is of the form
... op pack
where op
in our scenario is the comma operator, and pack
is the expression containing our tuple in an unexpanded context like:
(..., (std::cout << std::get<I>(myTuple))
So if I have a tuple like so:
auto myTuple = std::make_tuple(5, "Hello", -0.1);
And a std::integer_sequence
whose values are specified by a non-type template (see above code)
size_t... I
Then the expression
(..., (std::cout << std::get<I>(myTuple))
Gets expanded into
((std::cout << std::get<0>(myTuple)), (std::cout << std::get<1>(myTuple))), (std::cout << std::get<2>(myTuple));
Which will print
5Hello-0.1
Which is gross, so we need to do some more trickery to add a comma separator to be printed first unless it's the first element.
To accomplish that, we modify the pack
portion of the fold expression to print " ,"
if the current index I
is not the first, hence the (I == 0? "" : ", ")
portion*:
(..., (std::cout << (I == 0? "" : ", ") << std::get<I>(_tup)));
And now we'll get
5, Hello, -0.1
Which looks nicer (Note: I wanted similar output as this answer)
*Note: You could do the comma separation in a variety of ways than what I ended up with. I initially added commas conditionally after instead of before by testing against std::tuple_size<TupType>::value - 1
, but that was too long, so I tested instead against sizeof...(I) - 1
, but in the end I copied Xeo and we ended up with what I've got.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With