为什么(不相关的)using 声明可以通过参数依赖查找调和重载歧义?

Why can an (irrelevant) using declaration reconcile overload ambiguity with Argument-Dependent Lookup?

这是关于使用参数相关查找 (ADL) 函数重载的问题 的跟进。我想检查一下我在这种情况下对规则的理解,所以我写了一些测试代码。

首先,在 std 中没有 HasPtr class 的交换,当然,所以我写了一个我自己的命名空间,除了已经在全局范围内定义的交换之外,它还包含一个 HasPtr 版本的交换。 using 声明按我的预期工作——产生了歧义错误,因为已经定义了一个 HasPtr 版本的交换,如 "C++ Primer"、5ed.

中所做的那样

然后我想看看如果我将 using 声明更改为 using 指令会发生什么。书中说编译器将保持沉默,直到函数被实际调用。我想验证一下,所以代码如下:

#include <string>

class HasPtr {
public:
    friend void swap(HasPtr&, HasPtr&);
    std::string *ps;
};

void swap(HasPtr &lhs, HasPtr &rhs) {
    swap(lhs.ps, rhs.ps); // swap the pointers, not the string data
}

namespace myNS {
    void swap(HasPtr &lhs, HasPtr &rhs)     {
        std::string s = "in my name space";
        swap(lhs.ps, rhs.ps); // swap the pointers, not the string data
    }
}

class Foo {
    friend void swap(Foo &lhs, Foo &rhs);
    HasPtr h;
};


void swap(Foo &lhs, Foo &rhs) {
    using std::swap;  //<- commenting this line will cause error
    using namespace myNS;
    swap(lhs.h, rhs.h);
}

int main() {
    Foo f1, f2;
    swap(f1, f2);
}

奇怪的事情发生在第 27 行 (using std::swap;)。如果我将其注释掉,名称 myNS::swap 与已在全局范围中定义的签名完全相同,它被提升到全局范围,因此导致重载歧义的错误,正如我所料。

但是,如果我不注释第27行并编译,则不会报歧义错误。并且程序执行原本定义在全局范围内的::swap,就好像using指令using namespace myNS;没有提升myNS::swap一样,所以它不会被添加到重载的候选集中。我只是无法理解这种现象。为什么来自不相关命名空间的 using 声明(std 当然不包含交换的 HasPtr 版本)可以调和 ADL 下的重载歧义?为什么选择执行的是原来的 ::swap,而不是它在 myNS 中的竞争对手?第 27 行是否对重载过程有任何副作用(例如,从提升的命名空间中抑制名称,以便原始名称具有更高的优先级)?谢谢你的回答。

问题可以在 Visual Studio 2015 Update 3 Windows 7 和 GCC 4.8.4 ubuntu 14.04 中重现,均为 64 位。

注意 using-directive 和 using-declaration 有不同的效果:

(强调我的)

using-directive: From the point of view of unqualified name lookup of any name after a using-directive and until the end of the scope in which it appears, every name from ns_name is visible as if it were declared in the nearest enclosing namespace which contains both the using-directive and ns_name.

这意味着对于 using namespace myNS;,名称 myNS::swap 是可见的,就好像它是在全局范围内声明的一样。如果注释了using std::swap;,那么在全局范围内会找到2个swap,然后造成歧义。

如果 using std::swap; 未注释,std 命名空间中的 swap 将在函数范围内找到,然后 name lookup 停止,进一步的全局范围将不会'不被检查。请注意,全局 ::swap 可以通过 ADL 找到,加上 std::swap 它们将在重载决议中被考虑,并且 ::swap 被选择然后没有歧义。 myNS::swap 不会介入这种情况。

name lookup examines the scopes as described below, until it finds at least one declaration of any kind, at which time the lookup stops and no further scopes are examined.

这里的机制是三倍的。

  1. 一个使用声明,就像using std::swap是一个声明。它将 swap 的声明引入函数的声明区域。

  2. 另一方面,
  3. A 使用指令,不会将声明引入当前声明区域。它只允许非限定查找来处理指定命名空间中的名称,就好像它们是在当前声明区域的最近封闭命名空间中声明的一样。

  4. 较小声明区域中的声明隐藏较大封闭声明区域中的声明。

关于上述内容,您的设置方式如下:

  1. std::swapswap(Foo, Foo).
  2. 中声明
  3. myNS 中的名称可供 swap(Foo, Foo) 使用,就好像它们是在同一名称空间中用它声明的一样。
  4. 在#1 中添加的声明隐藏了在#2 中可见的声明。
  5. ::swap 可以被 ADL 找到(尽管也被 #1 隐藏),但是 myNS::swap 不能。由于 myNS 版本是隐藏的,ADL 也找不到它,因此它不会与任何内容发生冲突。

删除 std::swap 的声明后,现在 myNS::swap 可见。 ADL 也找到 ::swap,给你两个重载。它们都是有效的重载,并且会产生明显的歧义。