6

In c++17/g++7, there's finally the long missed ostream_joiner. It enables proper output to ostreams, separating collection elements with infix delimiters.

#include <algorithm>
#include <experimental/iterator>
#include <iostream>
#include <iterator>
#include <vector>
#include <string>

using string = std::string;
#if 1
struct pair {
    string first;
    string second;
};
#else
using pair = std::pair<string,string>;
#endif


std::ostream& operator<<(std::ostream& lhs, const pair &p) {
    return lhs << p.first << "=" << p.second;
}

int main()
{
    std::vector<pair> pairs = {{"foo", "bar"}, {"baz", "42"}};
    std::copy(std::begin(pairs),
          std::end(pairs),
          std::experimental::make_ostream_joiner(std::cout, ", "));
}

Whilst the code piece succesfully compiles and outputs ...

foo=bar, baz=42

... changing the #if 1 to a #if 0 in the snippet makes the compiler complaining about missing the proper shift operator:

main.cpp:29:70:   required from here
/usr/local/include/c++/7.2.0/experimental/iterator:88:10: error: no match for 
'operator<<' (operand types are 
'std::experimental::fundamentals_v2::ostream_joiner<const char*, char, 
std::char_traits<char> >::ostream_type {aka std::basic_ostream<char>}' and 
'const std::pair<std::__cxx11::basic_string<char>, 
std::__cxx11::basic_string<char> >')
  *_M_out << __value;

Does somebody have a clue why?

update

Barry has given the right answer to the question. It however does not solve the problem, and running a manual loop is not in the sense of reusing existing stl code, so the question gets extended to:

Is it possible to make the stream operator work without polluting the std namespace?

Community
  • 1
  • 1
argonaut6x
  • 115
  • 1
  • 8
  • this [could be a soultion](https://wandbox.org/permlink/h2NGXRT88q18q8m5), but I don't like it, since it touches `std` name space. – Marek R Sep 28 '17 at 13:15

2 Answers2

2

Somewhere inside of the implementation of ostream_joiner, there will be an attempt to something like:

os << value;

where os is a std::basic_ostream and value is your pair type. In order to determine what to do for that operator<< call, we lookup all the overloads operator<<() visible at the point of definition of this template as well as as the overloads in the associated namespaces of the arguments (this is known as argument-dependent lookup).

When you use your struct pair, the associated namespace of pair is ::, so ADL will find your ::operator<<(std::ostream&, pair const&). This overload works, is chosen, everything is happy.

When you use std::pair, the associated namespace of pair is std and there is no operator<<() that can be found that takes a std::pair. Hence the error.


You could instead create your own type in your own namespace for which you can add an overloaded operator<<, this can be fully your own type (the way it is in the question) or you could inherit the one in std:

struct pair : std::pair<string,string> {
    using std::pair<string,string>::pair;
};
std::ostream& operator<<(std::ostream&, my_pair const& ) {...}

Alternatively, you could just not use make_ostream_joiner. Could replace this:

std::copy(std::begin(pairs),
      std::end(pairs),
      std::experimental::make_ostream_joiner(std::cout, ", "));

with this:

const char* delim = "";
for (auto const& pair : pairs) {
    std::cout << delim << pair; // now, our point of definition does include
                                // our operator<<() declaration, we don't need ADL
    delim = ", ";
}
Barry
  • 286,269
  • 29
  • 621
  • 977
  • thanks, stupid me - accepted. However: do we need to pollute the namespace by inserting the stream operator into it, or give up here? – argonaut6x Sep 27 '17 at 18:56
  • @argonaut6x Adding new functions into namespace std is UB, so don't do that. – Barry Sep 27 '17 at 19:05
  • so we're giving up? No alternatives to make operator << with std::pair working? – argonaut6x Sep 27 '17 at 19:10
  • 1
    @argonaut6x Don't use `std::pair`? Use a for loop instead of `copy` and `make_ostream_joiner`? – Barry Sep 27 '17 at 19:19
  • 1
    well, I was asking for the reason why this did not work, and got a proper answer to that question. However, the suggestion to not use std::pair and / or running a loop instead of reusing stl code is counterproductive, imo. – argonaut6x Sep 28 '17 at 06:16
  • but it works for me under [clang](https://wandbox.org/permlink/QuippPuOcgKe6k9g) and [gcc](https://wandbox.org/permlink/5Pc0hvAojfSrzxwm). – Marek R Sep 28 '17 at 06:48
  • @MarekR Change your `#if 1` to `#if 0` – Barry Sep 28 '17 at 12:02
  • 1
    @argonaut6x Those are the two options. Not sure why you consider that "counterproductive." – Barry Sep 28 '17 at 12:10
  • @Barry I've missed that, sorry. Anyway this [could be a soultion](https://wandbox.org/permlink/h2NGXRT88q18q8m5), but I don't like it, since it touches `std` name space. – Marek R Sep 28 '17 at 13:16
0

Is it possible to make the stream operator work without polluting the std namespace?

If you're willing to replace your copy with a transform, then yes - with put_invocation taken from my answer here, you can use:

std::transform(
    std::begin(pairs), std::end(pairs),
    std::experimental::make_ostream_joiner(std::cout, ", "),
    [](auto& p) {
        return put_invocation([&p = p](auto& os) {
            // adl works just fine here
            return os << p;
            // or `os << p.first << "=" << p.second;`
        });
    }
);

This should optimize to exactly the same code as the version that injects into the std namespace.

Eric
  • 95,302
  • 53
  • 242
  • 374