当从std :: ostream_iterator调用时找不到operator <<的重载?

9
这个程序
// main.cpp

#include <iostream>
#include <utility>
#include <algorithm>
#include <iterator>
#include <map>

template<typename t1, typename t2>
std::ostream& operator<<(std::ostream& os, const std::pair<t1, t2>& pair)
{
  return os << "< " << pair.first << " , " << pair.second << " >";
}

int main() 
{
  std::map<int, int> map = { { 1, 2 }, { 2, 3 } };
  std::cout << *map.begin() << std::endl;//This works

  std::copy(
    map.begin(),
    map.end(),
    std::ostream_iterator<std::pair<int,int> >(std::cout, " ")
  ); //this doesn't work
}

出现了错误

无匹配的运算符'<<' (操作数类型为'std::ostream_iterator<std::pair<int, int> >::ostream_type {aka std::basic_ostream<char>} '和'const std::pair<int, int>')

我猜这不起作用是因为我的重载在 std::copy 内不可用,但是为什么呢?


1
我不知道为什么人们要对这个问题进行负投票,它的意图很明确,尽管标题可以更好地书写。 - Filip Roséen - refp
@ Filip Roséen - refp 简单来说,论坛中有很多白痴喜欢踩负面评价。问题在于,在实例化模板类std::ostream_iterator时,会在命名空间std::中搜索适当的运算符函数。由于在命名空间std中已经声明了operator <<,因此搜索停止。编译器找不到全局命名空间中定义的运算符。 - Vlad from Moscow
@VladfromMoscow 那不是答案;我正在撰写详细说明。一旦完成,我会通知你和 Chris。 - Filip Roséen - refp
@Filip Roséen - refp 请查看模板实例化和名称搜索部分。 - Vlad from Moscow
@FilipRoséen-refp 非常感谢您的解释,我全都明白了。如果我的回答有点愚蠢,请原谅。 - Roger Rodriguez Texido
显示剩余2条评论
2个回答

10

解释

由于在namespace std(更具体地说是在std::ostream_iterator内部)以未限定的方式调用了operator<<,并且涉及的所有参数也在同一命名空间中声明,因此只有命名空间std将被搜索可能匹配的内容。


hackish解决方案

namespace std {
  template<typename t1, typename t2>
  std::ostream& operator<<(std::ostream& os, const std::pair<t1, t2>& pair)
  {
     return os << "< " << pair.first << " , " << pair.second << " >";
  }
}

注意:只能特化包含用户定义类型的模板,位于命名空间std中。因此,根据标准,上面的片段可能是不正确的(如果std :: pair 不是用户声明的类型,请参见this discussion)。

详细解释

下面我们有一个命名空间N,它将帮助我们尝试模拟您对命名空间std的使用,以及编译器在查找适合给定类型的重载时正在发生什么。

命名空间N

namespace N {
  struct A { };
  struct B { };

  void func (A value) { std::cout << "A"; }

  template<class T>
  void call_func (T value) { func (value); }
}

main.cpp

void func (N::B value) {
  std::cout << "B";
}

int main() {
  N::A a;
  N::B b;

  func (a);         // (1)
  func (b);         // (2)

  N::call_func (a); // (3a)
  N::call_func (b); // (3b)
}

  1. 如果不了解参数依赖查找,可能会惊讶于编译器能够找到所需的重载以使(1)正常工作。

    ADL规定,在函数调用中使用未限定名称时,不仅会搜索当前命名空间以寻找合适的重载,还会搜索参数的命名空间;这就是编译器如何找到N::func的方式,即使我们没有明确地写出来。

  2. 当前命名空间中有一个合适的重载;一切都很顺利。

  3. ...

    为什么(3a)可以编译,而(3b)会导致错误诊断?

    当我们实例化模板N::call_func<T>时,它将尝试将类型为T的参数传递给名为func未限定函数。

    由于名称查找规则规定,在调用未限定名称的函数时,将搜索当前命名空间涉及参数的命名空间以寻找合适的匹配项,因此如果T是在命名空间N中声明的类型,则只会搜索命名空间N

    由于N::AN::B都在命名空间N中声明,因此编译器不会搜索任何其他作用域以找到合适的重载;这就是查找失败的原因。


我承认无法明确回答这个问题(尽管我仍然认为 OP 没有发布实际错误的信息),但请注意,OP 的函数重载和你的答案中放在“std”内的那个都是不合法的。 - chris
@juanchopanza 哦,太好了,已经修复了! - Filip Roséen - refp
@FilipRoséen-refp,嗯,这不会与“用户声明”的其他用途冲突吗?例如,“如果类X没有用户声明的构造函数,则将具有无参数的构造函数隐式声明为默认值(8.4)”。以std::string为例。使用此定义,没有用户声明的构造函数,但是隐式添加的构造函数会发生冲突。我可以接受你对“用户声明”进行歧义处理的论点(或者只是搞砸了,实际上只是用户定义)。无论如何,措辞看起来很好,我最好去看看实际上争论这个问题的那个问题。 - chris
@FilipRoséen-refp,好的,谢谢。[std-proposals]是我通常去的地方。 - chris
@chris 我们应该删除我们的评论,因为它们现在已经过时,或者与当前帖子无关。来到*C++*聊天室里逛逛吧; Lounge<C++>! - Filip Roséen - refp
重载运算符的限制与“用户声明”无关。要求是“至少有一个参数的类型是类、类的引用、枚举或枚举的引用”。您可能一直在考虑std模板的特化,但是operator<<是一个重载,而不是任何其他主模板的特化。 - aschepler

2

虽然这个问题已经得到了回答,但我想补充一下,有一种更好的方法可以打印地图而不滥用复制函数。 还有一个变换函数更适合做类似的事情。 我重新编写了你的示例,以提示如何使用转换函数将地图转换为字符串并将其打印到std::cout:

#include<iostream>
#include<map>
#include<algorithm>
#include<sstream>

template<typename t1, typename t2>
 std::ostream& operator<<(std::ostream& os, const std::pair<t1, t2>& pair)
 {
     return os << "< " << pair.first << " , " << pair.second << " >";
 }

std::string toString(const std::pair<int, int>& pair) {
    std::ostringstream str;
    str << "<" << pair.first << ", " << pair.second << ">";
    return str.str();
}

int main()
 {
  std::map<int, int> map = { std::make_pair(1, 2), std::make_pair(2, 3)};
  std::cout << *map.begin() << std::endl;//This works
  std::transform(map.begin(), map.end(),
  std::ostream_iterator<std::string>(std::cout, "\n"), toString);
}

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