cheek_i_breek's blog

By cheek_i_breek, 7 years ago, translation, In English

Printing containers in C++

Some useful facts:

  1. There is a std::basic_ostream class template in c++ standard library. It allows you to output some things like strings and numbers.
  2. Type of object std::cout is std::ostream wich is a typedef of std::basic_ostream <char> (also known as std::basic_ostream <char, std::char_traits <char>>).
  3. std::string is a typedef for std::basic_string <char> (also known as std::basic_string <char, std::char_traits <char>, std::allocator <char>>)

How to output with ostream

To output with ostream, operator << should be defined. Lets implement one for outputting pair <int, int> and test it.

ostream & operator << (ostream & os, pair <int, int> const& x) {
    os << x.first << ", " << x.second;
    return os;
}
Test

How to output container object ? Let's implement outputting for vector:

template <class T>
ostream & operator << (ostream & os, vector <T> const& x) {
    os << "{ ";
    for(auto& y : x) os << y << " ";
    return os << "}";
}
Test

And now for set:

template <class T>
ostream & operator << (ostream & os, set <T> const& x) {
    os << "{ ";
    for(auto& y : x) os << y << " ";
    return os << "}";
}
Test

And now for both:

template <class Container>
ostream & operator << (ostream & os, Container const& x) {
    os << "{ ";
    for(auto& y : x) os << y << " ";
    return os << "}";
}

Oops

error: ambiguous overload for 'operator<<' (operand types are 'std::ostream {aka std::basic_ostream}' and 'const char [3]')

The error happens here os << "{ ";, here os << y << " "; and here os << "}";.

Somewhere deep in standard library header files many operators << for outputting basic_string and const char* are defined like this:

...
template <class CharT, class Traits, class Allocator>
basic_ostream <CharT, Traits> &
    operator << (basic_ostream <CharT, Traits> & os, const basic_string <CharT, Traits, Allocator> & str);
...
template< class CharT, class Traits >
basic_ostream <CharT, Traits> & operator << (basic_ostream <CharT, Traits> & os, const char* s );
...

And they are conflicting with our container outputting operator.

The main part of this post

Look at this code.

template <class T>
void f (T const& v) {
    cout << "T \n";
}
template <class T>
void f (vector <T> const& v) {
    cout << "vector <T> \n";
}
void f (vector <int> const& v) {
    cout << "vector <int> \n";
}
Test

There are 3 overloads of one function f and they are not conflicting. The first one can take any possible argument. The second one is only for vectors. The third overload is for vector <int> objects only. For any argument compiler can choose which overload is more suitable. Everything looks pretty clear.

But what if we have 2 arguments ?

template <class T>
void f (vector <T> const& v, set <int> const& s) {
    cout << "vector <T> and set <int> \n";
}
template <class T>
void f (vector <int> const& v, set <T> const& s) {
    cout << "vector <int> and set <T> \n";
}
void test() {
    auto set_str = set <string> {"1", "2", "3"};
    auto set_int = set <int> {1, 2, 3};
    auto vec_str = vector <string> {"1", "2", "3"};
    auto vec_int = vector <int> {1, 2, 3};

    f( vec_str, set_int );
    f( vec_int, set_str );
}
/* output:
vector <T> and set <int>
vector <int> and set <T> */

There is a case when both overloads are equally suitable. If we try to call f( vec_int, set_int ) we'll get this: error: call of overloaded 'f(std::vector<int>&, std::set<int>&)' is ambiguous.

Back to the <<

So there are some << operators:

/* standart operator */
template <class CharT, class Traits, class Allocator>
basic_ostream <CharT, Traits> &
    operator << (basic_ostream <CharT, Traits> & os, const basic_string <CharT, Traits, Allocator> & str);

/* standart operator */
template <class CharT, class Traits>
basic_ostream <CharT, Traits> & operator << (basic_ostream <CharT, Traits> & os, const char* s);

/* our operator */
template <class Container>
ostream & operator << (ostream & os, Container const& x);

If we try to output something which is not string (basic_string) or const char* everything is ok.

But if we try to cout << "heh" we get the situation as described above. Since cout hase type ostream(basic_ostream <char>) our operator is more sutable than standart operators with basic_ostream <CharT, Traits>. At the same time const char* is more sutable for "heh" than Container const&.

To fix our problem all we need to do is to replace ostream by basic_ostream <Ch, Tr>.

template <class Ch, class Tr, class Container>
basic_ostream <Ch, Tr> & operator << (basic_ostream <Ch, Tr> & os, Container const& x) {
    os << "{ ";
    for(auto& y : x) os << y << " ";
    return os << "}";
}
Test

What about associative containers ? We can't output them yet. The main feature of associative containers is to store pairs of key and value, mapped to it. For example we have some map object map <int, int> m. What we get by dereferensing iterator to first element of our map (in code *m.begin()) is literally pair of key and value pair <const int, int> &. Let's implement outputting for pairs.

template <class X, class Y>
ostream & operator << (ostream & os, pair <X, Y> const& p) {
	return os << "[ " << p.first << ", " << p.second << "]" ;
}

Now we can output such things as map, unordered_map and even __gnu_pbds::tree and __gnu_pbds::cc_hash_table from ext/pb_ds/assoc_container.hpp.

Test

Full text and comments »

  • Vote: I like it
  • +25
  • Vote: I do not like it