overloading operator << for std::tuple - possible simplications?
Asked Answered
H

6

7

I used an answer to the SO question "iterate over tuple" to write a method to overload <<. This method was tested and appears to work correctly with g++ 4.7 on Debian squeeze.

However this method is kind of roundabout, since it seems << cannot be explicitly instantiated (I found a post about it here). So, one is forced to define a string method and then call that. I have a similar method for vector, which is more direct. Does anyone have suggestions about how to eliminate the extra step of creating a string method, using the same approach, or otherwise? Thanks in advance.

#include <tuple>
#include <iostream>
#include <string>
#include <sstream>
#include <vector>

using std::ostream;
using std::cout;
using std::endl;
using std::vector;
using std::string;

// Print vector<T>.
template<typename T> ostream& operator <<(ostream& out, const vector<T> & vec)
{
  unsigned int i;
  out << "[";
  for(i=0; i<vec.size(); i++)
    {
      out << vec[i];
      if(i < vec.size() - 1)
    out << ", ";
    }
  out << "]";
  return out;
}

////////////////////////////////////////////////////////////////

// Print tuple.
template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I == sizeof...(Tp), string>::type
stringval(const std::tuple<Tp...> & t)
{
  std::stringstream buffer;
  buffer << "]";
  return buffer.str();
}

template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I < sizeof...(Tp), string>::type
stringval(const std::tuple<Tp...> & t)
{
  std::stringstream buffer;
  size_t len = sizeof...(Tp);
  if(I==0)
      buffer << "[";
  buffer << std::get<I>(t);
  if(I < len - 1)
    buffer << ", ";
  buffer << stringval<I + 1, Tp...>(t);
  return buffer.str();
}

template<typename... Tp> ostream& operator <<(ostream& out, const std::tuple<Tp...> & t)
{
  out << stringval(t);
  return out;
}

int
main()
{
  typedef std::tuple<int, float, double> T;
  std::tuple<int, float, double> t = std::make_tuple(2, 3.14159F, 2345.678);
  cout << t << endl;
}

When compiled, this gives

[2, 3.14159, 2345.68]
Hypothermal answered 12/2, 2012 at 9:25 Comment(2)
Check the post on pretty-printing STL containers, including std::tuple.Heti
@Xeo: Thanks, that looks interesting.Hypothermal
F
5

You can just pass the std::ostream& into that stringval function and use out << instead of buffer <<.

Demo:

#include <tuple>
#include <iostream>
#include <type_traits>

template <size_t n, typename... T>
typename std::enable_if<(n >= sizeof...(T))>::type
    print_tuple(std::ostream&, const std::tuple<T...>&)
{}

template <size_t n, typename... T>
typename std::enable_if<(n < sizeof...(T))>::type
    print_tuple(std::ostream& os, const std::tuple<T...>& tup)
{
    if (n != 0)
        os << ", ";
    os << std::get<n>(tup);
    print_tuple<n+1>(os, tup);
}

template <typename... T>
std::ostream& operator<<(std::ostream& os, const std::tuple<T...>& tup)
{
    os << "[";
    print_tuple<0>(os, tup);
    return os << "]";
}
Franconian answered 12/2, 2012 at 10:40 Comment(5)
Thanks, Kenny. This is certainly an improvement over my version. I didn't think of passing the ostream in. So, it is not possible to do any better? It seems (for reasons I'm unclear about) that explicitly instantiating << is not possible (as I mentioned in the question). Two minor comments. <type_traits> is not necessary here. And I notice you removed the default from n, and called print_tuple explicitly with n=0. Is that just a style thing?Hypothermal
@FaheemMitha: Why do you want to explicitly instantiate the operator <<?Franconian
Well, it's not important, but it seems a natural thing to do, and would simplify the code.Hypothermal
@FaheemMitha: That's totally unnatural. What do you mean by explicit instantiation?Franconian
@FaheemMitha: You can't do that. If you need a specific tuple type, make it std::cout << std::tuple<int, char, double>(1, '2', 3.0)Franconian
F
3

Non-recursive C++17-way solution based on fold expressions (C++17), index sequences (C++14), lambda-functions and template parameter packs (both C++11):

#include <tuple>
#include <iostream>
#include <ostream>
#include <utility>

template< typename F, typename ...types >
F
for_all(F f, types &&... values)
{
    (f(std::forward< types >(values)), ...);
    return std::move(f);
}

template< typename F, typename ...types, std::size_t ...indices >
F
for_all_indices(F f, std::tuple< types... > const & t, std::index_sequence< indices... >)
{
    return for_all(std::move(f), std::get< indices >(t)...);
}

template< typename first, typename ...rest > // non-nullary tuples only
std::ostream &
operator << (std::ostream & out, std::tuple< first, rest... > const & t)
{
    //return ((out << std::get< first >(t)) << ... << std::get< rest >(t)); // simply prints extracted tuple elements w/o delimiters
    out << '[';
    for_all_indices([&out] (auto const & value) { out << value << ", "; }, t, std::index_sequence_for< rest... >{});
    return out << std::get< sizeof...(rest) >(t) << ']';
}

int
main()
{
    std::cout << std::make_tuple(1, 2.F, 3.0) << std::endl;
    return 0;
}

LIVE DEMO

Figone answered 26/10, 2015 at 11:1 Comment(18)
Thank you for the answer. This does not work on g++ 4.9 or clang 3.5, both default versions on Debian jessie. What versions of g++ and clang are required for this to work?Hypothermal
@FaheemMitha clang 3.7 from demo below the answer, but 3.6 should works too, I think.Figone
Ok. Any idea about g++? I'm coincidentally building clang 3.7 now.Hypothermal
I don't understand the question. Are you asking what error I get with g++ 4.9?Hypothermal
@FaheemMitha you need -std=c++1z flag, not -std=c++11.Figone
g++ -std=c++1z -o foo foo.cc g++-4.9.real: error: unrecognized command line option ‘-std=c++1z’Hypothermal
@FaheemMitha It seems you use outdated gcc.Figone
As I said above, it's 4.9.Hypothermal
@FaheemMitha OK, the answer is useless.Figone
Why is the answer useless?Hypothermal
@FaheemMitha During about 12 hours we can't compile it.Figone
That does not make the answer useless. I tried building clang 3.7 from Debian unstable sources, but have been repeatedly running out of space. I would have thought that 32 GB was enough (that's how much space it was using) but apparently not. :-( When I get it built, I'll try your code. Thanks.Hypothermal
@FaheemMitha Good luck. All my recent attemptions (on Ubuntu) to build clang of version higher then 3.6 are failed, due to CMakeLists.txt errors connected with llvm-tblgen using during building.Figone
That's annoying. But I wrote to the Clang Debian devs, and the dev who replied, pointed me to llvm.org/apt. Apparently the entire LLVM/Clang takes 38 GB, which is why I was running out of space. I think I had problems with those LLVM-provided debs before, but I'll try them again.Hypothermal
I recommend reporting build errors either to LLVM upstream (#llvm on OFTC is a reasonable way), or by emailing the Debian devs. Though the latter may not be an option if you are using Ubuntu. Can you build on Debian?Hypothermal
I tried with clang 3.7, and clang++-3.7 -std=c++1z -o foo foo.cc works. Thanks.Hypothermal
@FaheemMitha Congratulations. Your success inspired me to take my own attempt to build clang from trunk.Figone
Well, you could just install Clang from the same page - llvm.org/apt. It also has Ubuntu packages.Hypothermal
F
3

C++17 solution.

namespace std {

// Print tuple to ostream.
template<typename... Args>
ostream& operator<<(ostream& os, tuple<Args...> const& t)
{
  bool first = true;
  apply([&](auto&&... args){ ((os << (first ? "" : ", ") << args, first = false), ...); }, t);
  return os;
}

} // namespace std

This has to be put in namespace std in order for ADL to work. In fact every operator<< for a type in namespace N should be defined in namespace N. In that case ADL will cause the compiler to find it.

Needs headers

#include <iostream>
#include <tuple>

Online example here.

Forgiven answered 5/2, 2022 at 12:33 Comment(2)
What do you call this ( (expressions), ...) construct? I don't even know how to search for it in cppreference.Chinkiang
It's called a fold expression. en.cppreference.com/w/cpp/language/foldForgiven
P
1

Probably you don't need C++17 (which is not yet released) to attain a non-recursive (actually recursive, but in a more natural way) solution. I.e., you don't need fold expressions, and only need index sequences (C++14) and template parameter packs (C++11).

#include <iostream>
#include <sstream>
#include <utility>
#include <tuple>
#include <string>

template<class T>
std::ostringstream& concat_to_stream(std::ostringstream &oss, T &&arg) {
  oss << arg;
  return oss;
}

template<class First, class ...Rest>
std::ostringstream& concat_to_stream(std::ostringstream &oss, First &&firstArg, Rest &&... restArgs) {
  oss << firstArg << ", ";
  return concat_to_stream(oss, std::forward<Rest &&>(restArgs)...);
}

template<class ...Types>
std::string concat_to_string(Types &&... args) {
  std::ostringstream oss;
  oss << '[';
  concat_to_stream(oss, std::forward<Types &&>(args)...);
  oss << ']';
  return oss.str();
}

template<class Tuple, size_t... Indices>
std::string help_concat(const Tuple &tuple, std::index_sequence<Indices...>) {
  return concat_to_string(std::get<Indices>(tuple)...);
};

template<class ...Types>
std::string tuple_to_string(const std::tuple<Types...> &tuple) {
  return help_concat(tuple, std::make_index_sequence<sizeof...(Types)>{});
};

template<class ...Types>
std::ostream &operator<<(std::ostream &os, const std::tuple<Types...> &tuple) {
  return os << tuple_to_string(tuple);
}

int main() {
  std::tuple<int, double, std::string> sample_tuple = std::make_tuple(3, 1.723, "Hi!");
  std::cout << sample_tuple << '\n'; // [3, 1.723, Hi!]
  return 0;
}

The recursive part is the concat_to_stream part, which is pretty natural and common. The key part is help_concat, which I learn from Implementing std::tuple From The Ground Up: Part 6, tuple_cat Take 1.

The technique is to use an dummy std::index_sequence in the parameter list to deduce size_t... Indices in the template parameter list, allowing us to "flat" the contents of std::tuple into a variadic parameter list, which can be accepted by the concat_to_string function.

Portative answered 20/4, 2016 at 22:34 Comment(4)
Thanks for the answer. What flags would I need to pass to either gcc or clang to make this work? I have clang 3.7 installed, and gcc 4.9.Hypothermal
@FaheemMitha -std=c++14, I think this one is the only one worth mentioning.Portative
Both g++ 2.9 and clang++ 3.5 work with the -std=c++14 flag. Thanks.Hypothermal
I like your implementation above. I tested it normally and it works; however when I try to overload an ostream operator within a variadic class template that will print its member tuple; I get compiler errors. How would you go about using what you have here defined as is, then be able to use this within a variadic class template?Synapsis
P
0

Here is a non-recursive version, by using std::integer_sequence and some other related techniques.

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

template<class Ch, class Tr, class... Args>
decltype(auto) operator<<(std::basic_ostream<Ch, Tr>& os,
                          const std::tuple<Args...>& t)
{
    os << "(";
    print_tuple_impl(os, t, std::index_sequence_for<Args...>{});
    return os << ")";
}

originally it's from here: http://en.cppreference.com/w/cpp/utility/integer_sequence

Presentable answered 6/9, 2016 at 15:47 Comment(2)
Hi. If you post a complete compilable version, I'll try running it. Thanks.Hypothermal
Just refer to the link.Presentable
P
0

There is a neat trick that allows you to get all the elements of a tuple as individual variables. This allows you to write it like this:

#include <iostream>
#include <cstdint>
#include <sstream>
using namespace std;

template<typename... Args>
istream& operator>>( istream& in, tuple<Args...>& tup ) {
    auto func = [&]( auto&... args ) {
        ( in >> ... >> args );
    };
    apply( func, tup );
    return in;
}

template<typename... Args>
ostream& operator<<( ostream& out, const tuple<Args...>& tup ) {    
    auto func = [&]( const auto&... args ) {
        auto i = sizeof...(args);
        out << "{ ";
        ( (out << args << ( --i > 0 ? ", " : "") ), ... );
        out << " }";
    };
    apply( func, tup );
    return out;
}

int main() {
    auto data = "Hello 42"s;
    stringstream in(data);
    tuple<string, uint32_t> tup;
    in >> tup;
    cout << "The tuple: " << tup << '\n';
}
Plater answered 18/4, 2023 at 7:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.