【问题标题】:Confusion around function call resolution对函数调用解析的困惑
【发布时间】:2015-07-13 23:15:01
【问题描述】:

这个问题的灵感来自this one。考虑代码:

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

经过 GCC 的一些测试,我发现 swap(a, b); 解析为
1) std::swap 如果 T 已重载 std::swap(例如,标准容器类型)
2) ns::swap 否则会导致无限递归。
因此,编译器似乎会首先尝试在命名空间ns 中找到匹配项。如果找到匹配项,则搜索结束。但是当 ADL 进来的时候就不是这样了,在这种情况下,std::swap 无论如何都找到了。解决过程似乎很复杂。

我想知道在上述上下文中解析函数调用swap(a, b) 的过程中的幕后细节。参考标准将不胜感激。

【问题讨论】:

  • 请注意,using namespace std; 等使用指令的行为非常特殊;他们没有使用 using std::swap 等声明。
  • @dyp 对此深有感触 :-)
  • 据我所知,这个 using 指令不会做任何事情。它使名称空间std 的名称可见,就好像它们在此处的全局名称空间中声明一样,但是全局名称空间中的名称swap 对于ns::swap 内的纯非限定查找是隐藏的,因为名称空间ns 已经包含一个名称swap的成员。
  • 所以,如果我理解正确,如果你把它改成使用 std::swap,我们会得到编译错误,因为编译器会同时看到 std::swap 和 ns::swap。
  • @AntonFrolov 这里涉及到两种名称查找机制:纯非限定查找和参数相关查找。纯非限定查找将在找到名称的下一个封闭范围内停止。使用using std::swap;,我们告诉它在块范围内停止 - 将找不到ns::swap,它是封闭命名空间范围的成员。另一方面,依赖于参数的查找搜索与参数类型相关的所有范围。

标签: c++ language-lawyer function-calls overload-resolution argument-dependent-lookup


【解决方案1】:

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 std::swap;,实际上将名称引入它们出现的范围,而不是某些封闭范围。


诸如swap(a, b) 之类的函数调用表达式的查找称为非限定查找。标识符swap 没有被任何命名空间或类名限定,而ns::swap 则通过ns:: 限定。对潜在函数名称的非限定查找由两部分组成:纯非限定查找和参数相关查找。

纯非限定查找在最近的包含名称的封闭范围处停止。在 OP 的示例中,如上所示的等效转换所示,包含名称 swap 的声明的最近范围是命名空间 ns。不会搜索全局范围,不会通过纯非限定查找找到std::swap

依赖于参数的查找搜索与参数类型关联的所有范围(此处:仅命名空间和类)。对于类类型,已在其中声明类的命名空间是关联范围。 C++ 标准库的类型,例如std::vector&lt;int&gt; 与命名空间std 相关联,因此如果T 是C++ 标准库类型,则可以通过表达式swap(a, b) 的参数相关查找找到std::swap。同样,您自己的类类型允许在它们已声明的命名空间中找到swap 函数:

namespace N2 {
    class MyClass {};
    void swap(MyClass&, MyClass&);
}

因此,如果依赖于参数的查找找不到比纯非限定查找更好的匹配项,您最终将递归调用 ns::swap


调用swap unqualified(即swap(a, b)而不是std::swap(a, b))背后的想法是假设通过参数相关查找找到的函数比std::swap更专业。为您自己的类模板类型特化一个函数模板,例如 std::swap 是不可能的(因为禁止部分函数模板特化),并且您不能将自定义重载添加到命名空间 stdstd::swap 的通用版本通常实现如下:

template<typename T>
void swap(T& a, T& b)
{
    T tmp( move(a) );
    a = move(b);
    b = move(tmp);
}

这需要一个移动构造加上两个移动分配,甚至可能回退到副本。因此,您可以在与这些类​​型关联的命名空间中为您自己的类型提供一个专门的交换函数。您的专用版本可以利用您自己的类型的某些属性或私有访问权限。

【讨论】:

  • 你的速度更快,更详细,+1。
  • @Angew 我可能刚开始比较早;)
  • 我只是认为为自定义类型重载std::swap 是标准做法。
  • @Lingxi 程序不得将定义或声明添加到std 命名空间“除非另有说明”[namespace.std]p1。 std::swap 没有这样的例外。我知道的常见做法是将swap 定义为非成员友元函数,可能在类体内定义。另请参阅stackoverflow.com/a/5695855 C++ 标准库定义的类型确实重载std::swap,但它们处于作为标准一部分的独特位置 - 并且std 是它们关联的命名空间(所以这遵循上述常见做法)。
【解决方案2】:

标准中最重要的部分是 7.3.4/2(引用 C++14 n4140,强调我的):

using-directive 指定指定命名空间中的名称可以在 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

【讨论】:

    猜你喜欢
    • 2021-03-30
    • 2017-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-27
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    相关资源
    最近更新 更多