user4861515
user4861515

Reputation:

C++ overloading ostream << for arbitrary collection

I'm trying to overload the << operator so I can do, for example,

list<string> string_list = ...;
vector<double> double_vector = ...;
set<list<int>> int_list_set = ...;
cout << string_list << double_vector << int_list_set << endl;

Another user of this site, Chris Redford, posted some helpful code for doing this with vectors at How to print out the contents of a vector? . I've attempted to adapt his code to work with other types of collection as follows:

template <template <typename...> class collection, typename T>
std::ostream& operator<<(std::ostream& out, const collection<T>& c)  {
  out << "[ ";
  out << *c.begin();
  for(auto it=next(c.begin(),1); it!=c.end(); ++it) {
    out << " , ";
    out << *it;
  }
  out << " ]";
  return out;
}

Obviously I'm a noob when it comes to writing templates, so any tips for reading materials would be welcome. Hopefully it is clear that I'd like this to work for anything that can do .begin() and .end(). When compiling this with

int main(int argc, char **argv) {
  list<string> words;
  words.push_back("hello");
  words.push_back("world");
  cout << words << endl;
}

, I get a compiler error saying "ambiguous overload for 'operator<<'" and a bunch of gobbledy goop I don't understand. I think gcc might be trying to redefine what << means for std::string, but I'm not sure. Ideally, I'd like to tell the compiler not to try to redefine this operation for types for which it is already defined. I'm also using -std=C++14 so I'm open to clever use of auto. Any suggestions?

Edit: corrected bad use of T... in original question.

Upvotes: 0

Views: 638

Answers (3)

Jonathan Wakely
Jonathan Wakely

Reputation: 171303

There's a problem with your function:

template <template <typename...> class collection, typename T>
std::ostream& operator<<(std::ostream& out, const collection<T>& c)  {

That will try to print any type which is a specialization of a template! So it will be used for std::pair<int, double> and other non-containers, and will fail to compile because they don't have begin() and end() member functions.

The solution is to constrain the template so it only matches types that can be iterated over. This is based on my code in <redi/printers.h> and will print anything that can be passed to std::begin:

  template<typename Range,
           typename = decltype(*std::begin(std::declval<Range>()))>
    std::ostream&
    operator<<(std::ostream& os, const Range& range)
    {
      os << '[';
      const char* sep = "";
      for (auto& e : range)
      {
        os << sep << e;
        sep = ", ";
      }
      os << ']';
      return os;
    }

You still have the problem that this function will match for types which already have their own overloaded operator<< and so will either do the wrong thing or will be ambiguous (that is a problem for std::string, for example).

In <redi/printers.h> I solve that by defining a different function to do the printing, called print_one, and the overload that prints ranges is disabled if the type can already be printed using operator<<.

Upvotes: 0

Joerg S
Joerg S

Reputation: 5129

Just found following: Pretty-print C++ STL containers

Solution looks quite sophisticated. If you want to go for some more simple solution you could do the following:

Writing a template operator<< is very likely to conflict with any exisiting declaration of operator<<. What you could do is use a print function as already proposed and write some smaller wrappers, e.g.:

template <class collection>
std::ostream& printCollection (std::ostream& out, const collection& c)  {
  out << "[ ";
  out << *c.begin();

  for(auto it = next(c.begin(), 1); it != c.end(); ++it) {
    out << " , ";
    out << *it;
  }
  out << " ]";
  return out;
}

template <typename T>
std::ostream& operator<< (std::ostream& os, std::list<T>& collection) {
  return printCollection(os, collection);
}

// ...

Upvotes: 2

Surt
Surt

Reputation: 16099

I've made something like that for a another project, but I had to make an "<<" for each type and not all types are implemented here.

#include <iostream>
#include <vector>
#include <list>
#include <utility>
#include <algorithm>
#include <string>
#include <array>
#include <set>
#include <map>
#include <assert.h>

using namespace std;

int indentCnt = 0;
static string indents = "  ";
class AddOne {
    int& counter;
public:
    AddOne(int& pre = indentCnt) : counter(pre) {
        counter++;
        if (indents.length()<2*counter)
            indents += indents;
     }
    ~AddOne() {
        counter--;
    }
};

string indent() {
    assert(indents.length() >= 2*indentCnt);

    return indents.substr(0, 2*indentCnt);
}

enum delimiters { deBefore, deBetween, deAfter, deNum };
using delims = array<string, deNum>;

template<typename cType>
std::ostream& forallout(std::ostream& out, const cType& v, const delims& delim) {
    auto it = v.begin();
    out << delim[deBefore];
    if (it != v.end()) {
        for (; it != prev(v.end()); it++) // to avoid the seperator after last.
            out << *it << delim[deBetween];
        out << *it;
    } else
        out << "~Empty~";
    out << delim[deAfter];

    return out;
}

template <typename kType, typename dType>
std::ostream& operator<<(std::ostream& out, const std::map<kType, dType>& c)  {
    delims de { indent()+"[\n  "+indent(), ",\n  "+indent(), "\n"+indent()+"]" };
    AddOne aMap(indentCnt);

    return forallout(out, c, de);
}

template <typename dType>
std::ostream& operator<<(std::ostream& out, const std::vector<dType>& c)  {
    delims de { indent()+"[\n", ",\n", "\n"+indent()+"]" };
    AddOne aVec(indentCnt);

    return forallout(out, c, de);
}

template <typename dType>
std::ostream& operator<<(std::ostream& out, const std::list<dType>& c)  {
    delims de { indent()+"(", "<->", ")" };

    return forallout(out, c, de);
}

template <typename dType>
std::ostream& operator<<(std::ostream& out, const std::set<dType>& c) {
    delims de { indent()+"{", ", ", "}" };

    return forallout(out, c, de);
}

template <typename dType, typename kType>
std::ostream& operator<<(std::ostream& out, const std::pair<kType, dType>& c)  {
    delims de { "[", ":", "]" };

    out << de[deBefore] << c.first << de[deBetween] << c.second << de[deAfter];

    return out;
}

template <typename kType>
std::ostream& operator<<(std::ostream& out, const std::pair<kType, string>& c)  {
    delims de { "[", ":", "]" };

    out << de[deBefore] << c.first << de[deBetween] << "\"" << c.second << "\"" << de[deAfter];

    return out;
}

Upvotes: 0

Related Questions