如何在不同的命名空间中指定重载运算符?

6

我在使用C++标准库时遇到了麻烦。以下示例无法编译:(请注意,这是为了制作最小示例而削减的,因此它本身并没有太多意义)

#include <algorithm>
#include <string>
#include <vector>

namespace otherns {

class Property {
public:
  const std::string &getName() const { return m_name; }

private:
  std::string m_name;
};
}

bool operator==(const otherns::Property &a, const otherns::Property &b) {
  return a.getName() == b.getName();
}

/* Merge, second takes priority */
std::vector<otherns::Property>
merge_props(const std::vector<otherns::Property> &xs,
            const std::vector<otherns::Property> &ys) {
  std::vector<otherns::Property> ans = ys;
  for (const auto &x : xs) {
    if (std::find(ans.begin(), ans.end(), x) == ans.end()) {
      ans.push_back(x);
    }
  }
  return ans;
}

错误是“二进制'==':没有找到接受左操作数类型为'otherns :: Property'的运算符(或没有可接受的转换)”,它出现在std :: find 的实现中的某个位置。这是使用MSVC发生的,但我也尝试过clang和gcc,并获得了类似的结果。

以下代码可以正常工作:

std::vector<otherns::Property>
merge_props(const std::vector<otherns::Property> &xs,
            const std::vector<otherns::Property> &ys) {
  std::vector<otherns::Property> ans = ys;
  for (const auto &x : xs) {
    if (std::find_if(ans.begin(), ans.end(), [&x](const otherns::Property &y) {
          return x == y;
        }) == ans.end()) {
      ans.push_back(x);
    }
  }
  return ans;
}

我猜这似乎与ADL / Koenig查找有关,但我真的不明白为什么我的operator==没有找到。如果我想使用第一个更简单的find函数形式,最好的解决方案是什么?

实际上,otherns来自于第三方库的头文件,所以我不能将我的运算符放入该头文件中。


4
无法找到是因为参数依赖查找只会查找“将类添加到集合中的最内层命名空间”,由于你的运算符在全局命名空间中,所以从未被考虑。 - StoryTeller - Unslander Monica
3
因此,您必须始终在与类相同的名称空间中声明运算符。 - bolov
@PeterHull - 在我看来,lambda表达式是唯一可行的解决方案。毫无疑问,这可能会带来不便。也许联系库维护者并报告此问题可以帮助未来的用户(包括您自己,如果您在将来的项目中继续使用该库)。 - StoryTeller - Unslander Monica
@StoryTeller 很有趣。我会阅读的,但看起来你是对的。谢谢! - Alexander Oh
我会按照建议使用lambda表达式。我的operator==没有实现所谓的相等性(实际类Property有比名称更多的数据成员),所以我所做的有点欺骗/黑客行为。我认为不应该修改库。 - Peter Hull
显示剩余5条评论
3个回答

2
规则相当复杂,我自己也没有完全掌握,但让我们看看能否理解它们(我认为我们可以):
namespace nx {
struct X {};
}

namespace ns {    
auto foo(nx::X x1, nx::X x2) { return x1 == x2; }
// error: no match for 'operator==' (operand types are 'nx::X' and 'nx::X')
}

auto operator==(nx::X, nx::X) { return true; }

auto global_foo()
{
  return ns::foo(nx::X{}, nx::X{}); 
}

这个错误的原因很简单:operator==在使用前没有声明。这与ADL无关。到目前为止,一切都好理解。让我们来修复它:
namespace nx {
struct X {};
}

auto operator==(nx::X, nx::X) { return true; }

namespace ns {
auto foo(nx::X x1, nx::X x2) { return x1 == x2; }
}

auto global_foo()
{
  return ns::foo(nx::X{}, nx::X{}); 
}

这能行吗?是的,它可以编译并调用我们的operator==。这是正确的解决方案吗?不是!因为如果我们添加了这个:
namespace nx {
struct X {};
}

auto operator==(nx::X, nx::X) { return true; } // (1)

namespace ns {

template <class T> auto operator==(T, int) { return false; } // (2)

auto foo(nx::X x1, nx::X x2) { return x1 == x2; }
// error: no match for 'operator==' (operand types are 'nx::X' and 'nx::X')
}

auto global_foo()
{
  return ns::foo(nx::X{}, nx::X{});
}

然后,(2)在ns中隐藏了(1)在全局命名空间中,即使(1)更适合。这被称为名称隐藏,并且 - 再次强调 - 不涉及任何ADL。更糟糕的是:
namespace nx {
struct X {};
}

auto operator==(nx::X, nx::X) { return true; } // (1)

namespace ns {

template <class T> auto operator==(T, T) { return false; } // (2)

auto foo(nx::X x1, nx::X x2) { return x1 == x2; } // calls (2)
}

auto global_foo()
{
  return ns::foo(nx::X{}, nx::X{}); 
}

将英文翻译成中文:

编译时会静默调用(2)而不是我们的运算符(1)

在现实世界的情况下,将namespace ns视为命名空间std和声明在std内部的任何运算符。这就是您帖子中的情况。

正确的解决方案是:

namespace nx {
struct X {};
auto operator==(nx::X, nx::X) { return true; } // (1)
}

namespace ns {

template <class T> auto operator==(T, T) { return false; } // (2)

auto foo(nx::X x1, nx::X x2) { return x1 == x2; } // calls (1)
}

auto global_foo()
{
  return ns::foo(nx::X{}, nx::X{}); 
}

这里发生的情况是,ADL启用并从nx中引入了(1),现在(1)与(2)一起被考虑。但(1)比(2)更专业化,因此正确地选择了(1)。
如果您无法控制namespace nx并且无法在其中添加运算符,则我建议使用可调用对象而不是依赖于运算符。例如,使用自己的谓词(lambda)的std::find_if而不是std::find,您可以完全控制要调用哪个方法/运算符。当我说“完全”时,我的意思是::operator==(x1, x2)(或您声明的任何其他命名空间)而不是x1 == x2。
您可以通过Herb Sutter的这篇优秀文章命名空间和接口原则了解更多内容。

1

只需在命名空间 otherns 中声明 operator== (查找将在命名空间作用域中进行)

namespace otherns {
bool operator==(const otherns::Property &a, const otherns::Property &b) {
  return a.getName() == b.getName();
}
}

可工作的代码

您可以在第三方库的单独头文件中完成此操作。


0

您在全局命名空间中定义了operator==(可能是由于错误的缩进而导致)。这样做将无法通过参数相关查找找到它。

该运算符应在与其参数(之一)相同的命名空间中声明:

namespace otherns {

    class Property {
    public:
        const std::string &getName() const { return m_name; }

    private:
        std::string m_name;
    };

    bool operator==(const otherns::Property &a, const otherns::Property &b) {
        return a.getName() == b.getName();
    }
}

那个小改变使你的示例能够干净地编译。


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