11

我发现 C++ 解析运算符重载的奇怪行为,我无法解释自己。指向一些描述它的资源的指针与答案一样好。

我有 2 个翻译单元。在一个(称为 util.cpp/h)中,我声明并定义了两个运算符(为了可读性,我省略了真正的实现,无论如何都会出现问题):

// util.h
#ifndef GUARD_UTIL
#define GUARD_UTIL

#include <iostream>

std::istream& operator>>(std::istream& is, const char* str);
std::istream& operator>>(std::istream& is, char* str);
#endif

和:

//util.cpp
#include "util.h"
#include <iostream>

std::istream& operator>>(std::istream& is, const char* str) {
  return is;  
}
std::istream& operator>>(std::istream& is, char* str) {
  return is;  
}

这些运算符当然是在全局命名空间中,因为它们对标准类型和内置类型进行操作,并且应该可以在任何地方使用。它们可以从全局命名空间(例如从 main())或显式告诉编译器它们在全局命名空间中正常工作(参见代码示例)。

在另一个翻译单元(称为 test.cpp/h)中,我在命名空间中使用这些运算符。这一直有效,直到我将类似的运算符放入此命名空间。一旦添加了此运算符,编译器(例如 gcc 或 clang)就无法再找到可行的运算符>>。

// test.h
#ifndef GUARD_TEST
#define GUARD_TEST

#include <iostream>

namespace Namespace {
  class SomeClass {   
    public:
      void test(std::istream& is);
  };

  // without the following line everything compiles just fine
  std::istream& operator>>(std::istream& is, SomeClass& obj) { return is; }; 
}

#endif

和:

//test.cpp
#include "test.h"
#include "util.h"
#include <iostream>

void Namespace::SomeClass::test(std::istream& is) {
  ::operator>>(is, "c"); //works
  is >> "c" //fails
}

为什么在命名空间中没有运算符>>时编译器会找到正确的运算符,但当有一个时却找不到?为什么即使它具有不同的签名,运算符也会影响编译器找到正确的能力?

解决此问题的一种尝试是

std::istream& operator>>(std::istream& is, const char* str) { ::operator>>(is, str); }

进入命名空间,但链接器抱怨以前的定义。所以附加:为什么链接器可以找到编译器找不到的东西?

4

3 回答 3

10

这是一个名称隐藏问题。标准说 (c++03, 3.3.7/1)

可以通过在嵌套声明区域或派生类 (10.2) 中显式声明相同名称来隐藏名称。

您的案例中的“名称”将是operator>>并且命名空间构成嵌套的声明性区域。

解决这个问题的最简单方法是使用using声明 namespace-local的声明operator<<

namespace your_namespece {
    std::istream& operator>>(std::istream& is, SomeClass& obj) { return is; }; 
    using ::operator>>;
}

请注意,此功能不会干扰 Koenig 查找(至少在您的情况下,原则上可以),因此std::仍然会找到 IO 运算符。

PS:解决此问题的另一种可能性是将运算符定义SomeClassinlinefriend。此类函数在命名空间级别(在“他们的”类之外)声明,但从那里不可见。它们只能通过 Koenig 查找找到。

于 2012-07-06T09:41:39.427 回答
3

这里有几个问题;对于初学者,您正在重新定义全局命名空间中已经存在的函数std::. 但是,您描述的问题是由于名称查找的工作方式。基本上,在运算符重载的情况下,编译器会进行两次名称查找。第一个(用于所有符号,而不仅仅是运算符)从符号出现的范围开始,向外工作:首先是本地块,然后是类及其基类(如果有),最后是命名空间到全局命名空间。这种查找的一个重要特征是它会在找到名称的任何范围内停止:如果它在本地范围内找到名称,则不会查找任何类;如果它在一个类中找到一个,它不会在基类或名称空间中查找,如果它在一个名称空间中找到一个,它不会在任何封闭的名称空间中查找。就此查找而言,所有重载都必须在同一范围内。第二次查找仅影响函数和运算符重载,并且发生在用作参数的类或对象的上下文中;因此,如果其中一个操作数是标准库中的类(或从标准库中的类派生的任何东西),编译器将在std::,即使使用该符号的上下文不包括std::. 您遇到的问题 char*,内置类型(例如. 您的功能都不在其中。如果要找到重载运算符,则必须在其操作数之一的范围内定义它。operator>>std::

Concretely, here: you can't overload std::istream& operator>>( std::istream&, char* ), because it is already overloaded in the standard library. std::istream& operator>>( std::istream&, char const* ) is possible, but I'm not sure what it's supposed to do, since it can't write to the second operand. More generally, you should only overload this operator for types that you have defined, and you should put your overload in the same namespace as the type itself, so that it will be found by the second lookup above (called Argument Dependent Lookup, or ADL—or earlier, Koenig lookup, after the person who invented it).

于 2012-07-06T10:38:14.073 回答
0

:: 是全局作用域,因此,编译器必须扫描全局命名空间并找到该运算符。是>>“C”,试图在命名空间中找到运算符>>,因此,编译器找到它并停止搜索,然后编译器尝试选择具有所需签名的运算符,如果没有这样的运算符 - 编译器失败。我认为您应该阅读 Herb Sutter Exceptional C++。

于 2012-07-06T09:40:36.963 回答