用户定义类的运算符重载的工作方式是通过参数相关查找。 ADL 允许程序和库避免因运算符重载而弄乱全局命名空间,但仍允许方便地使用运算符;也就是说,如果没有明确的命名空间限定,这与中缀运算符语法 a + b 是不可能的,而是需要正常的函数语法 your_namespace::operator+ (a, b)。
然而,ADL 并不只是到处搜索任何可能的运算符重载。 ADL 仅限于查看“关联”类和命名空间。 std::rel_ops 的问题在于,正如指定的那样,这个命名空间永远不能是标准库之外定义的任何类的关联命名空间,因此 ADL 不能与此类用户定义的类型一起使用。
但是,如果您愿意作弊,您可以让std::rel_ops 工作。
关联的命名空间在 C++11 3.4.2 [basic.lookup.argdep] /2 中定义。对于我们的目的,重要的事实是基类是其成员的命名空间是继承类的关联命名空间,因此 ADL 将检查这些命名空间是否有适当的功能。
所以,如果出现以下情况:
#include <utility> // rel_ops
namespace std { namespace rel_ops { struct make_rel_ops_work {}; } }
要(以某种方式)进入翻译单元,然后在支持的实现上(参见下一节),您可以定义自己的类类型,如下所示:
namespace N {
// inherit from make_rel_ops_work so that std::rel_ops is an associated namespace for ADL
struct S : private std::rel_ops::make_rel_ops_work {};
bool operator== (S const &lhs, S const &rhs) { return true; }
bool operator< (S const &lhs, S const &rhs) { return false; }
}
然后 ADL 将适用于您的类类型,并会在 std::rel_ops 中找到运算符。
#include "S.h"
#include <functional> // greater
int main()
{
N::S a, b;
a >= b; // okay
std::greater<N::s>()(a, b); // okay
}
当然,您自己添加make_rel_ops_work 在技术上会导致程序具有未定义的行为,因为C++ 不允许用户程序向std 添加声明。作为一个例子,说明这实际上是如何起作用的以及为什么,如果你这样做,你可能想去验证你的实现确实在这个添加下正常工作,考虑:
在上面我展示了make_rel_ops_work 的声明,它跟在#include <utility> 之后。有人可能会天真地认为,在 here 中包含这个并不重要,只要在使用运算符重载之前 sometime 包含标题,那么 ADL 就可以工作。规范当然没有这样的保证,并且在实际实现中并非如此。
clang 与 libc++,由于 libc++ 使用内联命名空间,将 (IIUC) 认为 make_rel_ops_work 的声明与包含 <utility> 运算符重载的命名空间不同的命名空间,除非 <utility> 的声明std::rel_ops 是第一位的。这是因为,从技术上讲,std::__1::rel_ops 和 std::rel_ops 是不同的命名空间,即使 std::__1 是一个内联命名空间。但如果 clang 发现 rel_ops 的原始命名空间声明位于内联命名空间 __1 中,那么它将把 namespace std { namespace rel_ops { 声明视为扩展 std::__1::rel_ops 而不是新命名空间。
我相信这个命名空间扩展行为是一种 clang 扩展,而不是由 C++ 指定的,因此您甚至可能无法在其他实现中依赖它。特别是 gcc 不会以这种方式运行,但幸运的是 libstdc++ 不使用内联命名空间。如果您不想依赖此扩展,那么对于 clang/libc++,您可以编写:
#include <__config>
_LIBCPP_BEGIN_NAMESPACE_STD
namespace rel_ops { struct make_rel_ops_work {}; }
_LIBCPP_END_NAMESPACE_STD
但显然你需要为你使用的其他库实现。我对make_rel_ops_work 的简单声明适用于clang3.2/libc++、gcc4.7.3/libstdc++ 和VS2012。