函数调用解析的混淆

7

这个问题的灵感来自于这个问题。考虑以下代码:

namespace ns {
  template <typename T>
  void swap(T& a, T& b) {
    using namespace std;
    swap(a, b);
  }
}

经过使用GCC的一些测试,我发现swap(a, b);解析结果如下:
1) 如果T有重载std::swap(例如:标准容器类型),则解析为std::swap
2) 否则解析为ns::swap,导致无限递归
因此,似乎编译器将首先尝试在命名空间ns中查找匹配项。如果找到匹配项,则搜索结束。但是当ADL参与时,情况并非如此,在这种情况下,std::swap仍将被找到。解析过程似乎很复杂。

我想了解在上述上下文中解析函数调用swap(a, b)的详细过程。请提供对标准的引用。


2
请注意,使用诸如 using namespace std; 的using-directives具有非常特殊的行为;它们不是使用声明(using-declarations),如 using std::swap - dyp
@dyp 刚刚有一种深刻的感觉 :-) - Lingxi
据我所知,这个using指令不会产生任何作用。它将命名空间std的名称视为在此处声明于全局命名空间中,但是全局命名空间中的swap名称在ns::swap内部进行纯未限定查找时被隐藏,因为命名空间ns已经包含了一个名称为swap的成员。 - dyp
所以,如果我理解正确的话,如果你改用std::swap,我们会得到编译错误,因为编译器会看到std::swap和ns::swap两个函数。 - Anton Frolov
@AntonFrolov 这里涉及到两种名称查找机制:纯未限定名称查找和参数相关名称查找。纯未限定名称查找将在找到名称的下一个封闭作用域处停止。通过 using std::swap;,我们告诉它在块作用域停止 - ns::swap 将不会被找到,因为它是封闭命名空间作用域的成员。另一方面,参数相关名称查找搜索与参数类型相关联的所有作用域。 - dyp
2个回答

6

OP中的代码与以下代码等价

using std::swap; // only for name lookup inside ns::swap

namespace ns {
  template <typename T>
  void swap(T& a, T& b) {
    swap(a, b);
  }
}

为什么?因为像using namespace std;这样的using-directives在C++14 [namespace.udir]p2中有一种非常奇特的行为:

using-directive指定了被提名命名空间中的名称可以在using-directive出现之后的范围内使用。在未经限定的名称查找中,这些名称似乎是在最近的封闭命名空间中声明的,该命名空间包含using-directive和被提名命名空间。

包含命名空间std和函数块作用域ns::swap的最近封闭命名空间是全局命名空间。
另一方面,using-declarationsusing std::swap;确实将名称引入它们出现的范围内,而不是某个封闭范围。
函数调用表达式(例如swap(a, b))的查找称为未经限定的查找。标识符swap没有与任何命名空间或类名限定,与ns::swap相反,ns::已通过限定限定。潜在函数名称的未经限定查找由两部分组成:纯未经限定查找和参数相关的查找。
纯未经限定查找在包含名称的最近封闭范围处停止。在OP的示例中,如上所示的等效转换所示,包含名称swap声明的最近范围是命名空间ns。全局范围不会被搜索,也不会通过纯未经限定查找找到std::swap
参数相关的查找搜索与参数类型相关联的所有范围(这里仅为命名空间和类)。对于类类型,类被声明的命名空间是关联作用域。C++标准库类型(例如std::vector<int>)与命名空间std相关联,因此,如果T是C++标准库类型,则可以通过参数相关的查找找到表达式swap(a, b)std::swap。同样,您自己的类类型允许在它们被声明的命名空间中找到swap函数:
namespace N2 {
    class MyClass {};
    void swap(MyClass&, MyClass&);
}

因此,如果参数相关查找没有找到比纯粹的非限定查找更好的匹配,你最终会递归地调用 ns::swap。
在调用未限定的 swap(即 swap(a, b) 而不是 std::swap(a, b))背后的想法是,通过参数相关查找找到的函数被认为比 std::swap 更专业。对于自己的类模板类型特化函数模板(由于禁止部分函数模板特化),无法实现,也不能向命名空间 std 添加自定义重载。std::swap 的通用版本通常实现如下:
template<typename T>
void swap(T& a, T& b)
{
    T tmp( move(a) );
    a = move(b);
    b = move(tmp);
}

这需要一个移动构造加上两个移动赋值,有可能甚至会回退到拷贝。因此,你可以为与这些类型相关联的命名空间提供一个专门的swap函数。你的专门版本可以利用你自己类型的某些属性或私有访问。


1
你既快又详细,加一分。 - Angew is no longer proud of SO
@Angew 我可能只是开始得更早一些 ;) - dyp
我认为重载std::swap以适用于自定义类型是标准做法。 - Lingxi
1
@Lingxi 程序不得向 std 命名空间“除非另有规定”[namespace.std]p1 添加定义或声明。对于 std::swap 没有这样的例外情况。我知道的常见做法是将 swap 定义为非成员友元函数,可能在类体内定义。另请参见 https://dev59.com/NW025IYBdhLWcg3w960W#5695855/ C++ 标准库定义的类型确实重载了 std::swap,但它们处于标准的独特位置 - std 是它们关联的命名空间(因此遵循前述的常见做法)。 - dyp

2
标准中最重要的部分是7.3.4/2(引用C++14 n4140,强调我的):
using-directive指定所提名名称空间中的名称可以在using-directive之后出现的范围内使用。在未限定名称查找(3.4.1)期间,名称出现为如果它们在最近包含using-directive和提名名称空间的封闭名称空间中声明一样。” using-directive位于:: ns中的函数内,并提名:: std。这意味着在未限定名称查找的目的下,该using-directive的效果是::std中的名称表现得就像它们在::中声明一样。特别地,不会像它们在::ns中一样。
因为未限定名称查找始于::ns中的函数,所以它会先搜索::ns,然后才查找::。并且它找到了::ns::swap,于是它停止在那里,而不会查找using-directive引入的::std::swap

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