std::experimental::ostream_joiner and std::pair

6
在c++17/g++7中,终于有了期待已久的ostream_joiner。它可以使集合元素与中缀分隔符正确地输出到输出流中。
#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, ", "));
}

尽管代码可以成功编译并输出结果...

foo=bar, baz=42

将代码片段中的 #if 1 改为 #if 0 会导致编译器报错,提示缺少正确的移位运算符:

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;

有人知道为什么吗?

更新

Barry给出了正确的答案,但它并没有解决问题,而手动循环并不是重用现有STL代码的意义,因此问题得到扩展:

是否可能使流操作符在不污染std命名空间的情况下工作?


这可能是一个解决方案,但我不喜欢它,因为它涉及到了 std 命名空间。 - Marek R
2个回答

2

ostream_joiner的实现中,会尝试进行如下操作:

os << value;

在这里,os 是一个 std::basic_ostream,而 value 则是你的 pair 类型。为了确定该 operator<< 调用要执行什么操作,我们会查找所有可见的 operator<<() 重载,包括此模板定义点处的重载以及参数的 相关命名空间 中的重载(这被称为 ADL)。

当你使用 你自己的 struct pair 时,pair 的相关命名空间是 ::,因此 ADL 将找到你的 ::operator<<(std::ostream&, pair const&)。该重载有效,并被选择,一切都很顺利。

当你使用 std::pair 时,pair 的相关命名空间是 std,并且没有可以接受 std::pairoperator<<() 可以被找到。因此出现了错误。


相反地,你可以在你自己的命名空间中创建自己的类型,并添加重载的 operator<<。这可以是完全属于你自己的类型(就像问题中那样),或者你可以继承 std 中的类型:

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

另外,您也可以选择不使用 make_ostream_joiner。可以将其替换为:

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

使用这个:

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 = ", ";
}

@argonaut6x 将新功能添加到命名空间 std 中是未定义行为,所以请不要这样做。 - Barry
1
@argonaut6x 不使用std::pair?使用for循环代替copymake_ostream_joiner - Barry
1
好的,我之前询问为什么这个方法不起作用,并得到了一个合适的答案。然而,建议不使用std::pair和/或运行循环而不是重复使用STL代码是适得其反的,我认为。 - argonaut6x
但在clanggcc下对我有效。 - Marek R
1
@argonaut6x 那就是两个选项。不确定为什么您认为那是“适得其反”的。 - Barry
显示剩余4条评论

0
有没有可能使流运算符在不污染std命名空间的情况下工作?
如果你愿意用transform替换你的copy,那么是的——使用从我的答案中获取的put_invocation,你可以使用:
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;`
        });
    }
);

这应该优化为与将代码注入到std命名空间的版本完全相同的代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接