Overloading << operator: struct with vector of structs

Viewed 254
  • Overloading << for vectors works.
  • Overloading << for custom structs works.
  • The combination works as well.

But if I use the << operator on a struct with a vector of structs, compilation fails. I made up a little example to showcase the problem:

#include <iostream>
#include <ostream>
#include <vector>

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

namespace xyz {

struct Item {
    int a;
    int b;
};

struct Aggregation {
    std::vector<Item> items; 
};

std::ostream& operator<<(std::ostream& out, const Item& item) {
    out << "Item(" << "a = " << item.a << ", " << "b = " << item.b << ")";
    return out;
}

std::ostream& operator<<(std::ostream& out, const Aggregation& agg) {
    out << "Aggregation(" << "items = " << agg.items << ")";
    return out;
}

}  // namespace xyz

int main() {
    xyz::Aggregation agg;
    agg.items.emplace_back(xyz::Item{1, 2});
    agg.items.emplace_back(xyz::Item{3, 4});

    std::cout << agg.items << std::endl;  // works: [Item(a = 1, b = 2), Item(a = 3, b = 4)]
    std::cout << agg << std::endl;        // fails, expected: Aggregation(items = [Item(a = 1, b = 2), Item(a = 3, b = 4))
}

Link to compiler explorer: https://godbolt.org/z/a8dccf

<source>: In function 'std::ostream& xyz::operator<<(std::ostream&, const xyz::Aggregation&)':
<source>:35:41: error: no match for 'operator<<' (operand types are 'std::basic_ostream<char>' and 'const std::vector<xyz::Item>')
   35 |     out << "Aggregation(" << "items = " << agg.items << ")";
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^~ ~~~~~~~~~
      |                           |                    |
      |                           |                    const std::vector<xyz::Item>
      |                           std::basic_ostream<char>
In file included from /opt/compiler-explorer/gcc-10.2.0/include/c++/10.2.0/iostream:39,
                 from <source>:1:
/opt/compiler-explorer/gcc-10.2.0/include/c++/10.2.0/ostream:108:7: note: candidate: 'std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(std::basic_ostream<_CharT, _Traits>::__ostream_type& (*)(std::basic_ostream<_CharT, _Traits>::__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; std::basic_ostream<_CharT, _Traits>::__ostream_type = std::basic_ostream<char>]'
  108 |       operator<<(__ostream_type& (*__pf)(__ostream_type&))
      |       ^~~~~~~~

What am I doing wrong?

2 Answers

In the main function, when you write this line:

std::cout << agg.items << std::endl;

the compiler will look in the global namespace for all overloads of operator<<. The correct overload is chosen via overload resolution, and so the call works.

When you write the similar code here

std::ostream& operator<<(std::ostream& out, const Aggregation& agg) {
    out << "Aggregation(" << "items = " << agg.items << ")";
    return out;
}

since this code is in namespace xyz, the compiler will first look up the overloads of operator<< in namespace xyz. Once it finds any overloads at all, it will stop looking for additional overloads. However, since the actual operator<< that you want is not in namespace xyz, overload resolution fails, and you get an error.

The fix for this is to simply move the operator<< taking a vector<T> into namespace xyz.

Here's a demo.


If you actually want an operator<< that takes a vector of any type to be accessible from the global scope as well as namespace xyz, then you can define it in the global scope as you have done in your question. Then just bring the operator into xyz, or preferably, into the specific functions in namespace xyz where you need them, like this:

namespace xyz 
{
  // using ::operator<<;  // if you want all of `xyz` to see the global overload
 
  std::ostream& operator<<(std::ostream& out, const Aggregation& agg) 
  {
    using ::operator<<;  // if you only want the global overload to be visible in this function
    out << "Aggregation(" << "items = " << agg.items << ")";
    return out;
  }

  // ...
}

Here's a demo that shows how to stream a vector<int> as well as a vector<xyz::Item>.


Thanks to @NathanPierson for pointing out that the using declaration can be local to the functions where it's needed, instead of polluting the entirety of namespace xyz.

I ran over a similar issue again with the fmt library (https://github.com/fmtlib/fmt/issues/2093). Another working solution seems to be adding operator<< overloading for std containers directly to namespace std:

namespace std {

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

}  // namespace std

Link to compiler explorer: https://godbolt.org/z/o7c9WP

I feel bad about adding something to namespace std though. Any thoughts on this?

Related