为什么两阶段查找无法选择'swap'的重载版本?

时间:2014-01-27 15:20:56

标签: c++ templates c++11 swap argument-dependent-lookup

我正在研究this fascinating answersubtle question关于为用户定义类型实施swap函数的最佳做法。 (我的问题最初是由discussion of the illegality of adding types to namespace std推动的。)

我不会在此处重新打印上述链接答案中的代码段。

相反,我想理解答案。

我上面链接的答案在第一个代码段下面指出了{strong>在swap 中重载namespace std(而不是专门化 >它在该命名空间中):

  

如果您的编译器打印出不同的东西,那么它就不会   正确实施模板的“两阶段查找”。

然后,答案会指出swap 中专门namespace std(与重载相反)会产生一个结果不同专业化时的理想结果)。

但是,答案还有另外一种情况:专门用于用户定义的模板类的交换 - 在这种情况下,再次无法实现所需的结果。

不幸的是,答案只是陈述事实;它没有解释为什么

有人可以详细说明这个答案,并在答案中提供的两个特定代码片段中描述查找过程:

  • swap中为用户定义的非模板类重载namespace std(如链接答案的第一个代码段)

  • swap中为用户定义的模板类专门设置namespace std(如链接答案的最终代码段)

在这两种情况下,都会调用泛型std::swap,而不是用户定义的swap。为什么呢?

(这将阐明两阶段查找的性质,以及best practice for implementing user-defined swap的原因;谢谢。)

2 个答案:

答案 0 :(得分:8)

有很多Standardese的序言

示例中对swap()的调用需要一个依赖名称,因为其参数begin[0]begin[1]取决于周围T的模板参数algorithm()功能模板。这些相关名称的两阶段名称查找在标准中定义如下:

14.6.4.2候选函数[temp.dep.candidate]

  

1对于函数调用,其中post fi x-expression是从属名称,   使用通常的查找规则找到候选函数(3.4.1,   3.4.2)除了:

     

- 对于使用非标准名称查找(3.4.1)的查找部分,只有模板定义中的函数声明   找到了上下文。

     

- 对于使用关联的查找部分   名称空间(3.4.2),只在其中找到函数声明   模板定义上下文或模板实例化上下文   找到。

不合格的查找由

定义

3.4.1 Unquali fi ed name lookup [basic.lookup.unqual]

  

1在3.4.1中列出的所有情况下,搜索范围为a   按各自类别列出的顺序进行声明;   名称查找在名称发现声明后立即结束。如果不   声明被发现,该程序是不正确的。

和参数依赖查找(ADL)为

3.4.2依赖于参数的名称查找[basic.lookup.argdep]

  

1当函数调用(5.2.2)中的postfix-expression为时   unqualified-id ,其他名称空间通常不考虑   可以搜索非限定查找(3.4.1),并在这些名称空间中,   命名空间范围的朋友函数或函数模板声明   (11.3)可能无法看到。这些修改   搜索取决于参数的类型(以及模板模板)   参数,模板参数的名称空间。)。

将标准应用于示例

第一个示例调用exp::swap()。这不是依赖名称,不需要两阶段名称查找。由于对swap的调用是合格的,因此会进行普通查找,该查找仅查找通用swap(T&, T&)函数模板。

第二个示例(@HowardHinnant称之为“现代解决方案”)调用swap()并且在与{{1}相同的命名空间中也有一个重载swap(A&, A&)生命(在这种情况下是全局命名空间)。因为对swap的调用是不合格的,普通查找和ADL都发生在定义点(再次只发现泛型class A),但另一个ADL发生在实例化时(即swap(T&, T&)正在调用exp::algorithm()),这会选择main(),这是在重载解析期间更好的匹配。

到目前为止一切顺利。现在对于安可:第三个示例调用swap(A&, A&)并在swap()内有一个专门化template<> swap(A&, A&)。查找与第二个示例中的查找相同,但现在ADL不会选择模板特化,因为它不在namespace exp的关联命名空间中。但是,即使专门化class A在重载解析期间不起作用,它仍然在使用点实例化。

最后,第四个示例调用template<> swap(A&, A&)并且swap()内的重载template<class T> swap(A<T>&, A<T>&)位于全局命名空间中的namespace exp。查找与第三个示例中的相同,并且ADL再次没有获取重载template<class T> class A,因为它不在类模板swap(A<T>&, A<T>&)的关联命名空间中。在这种情况下,也没有必须在使用点实例化的专业化,因此通用A<T>在这里被调用。

结论

即使您不允许向swap(T&, T&)添加新的重载,也只允许显式特化,但由于两阶段名称查找的各种复杂性,它甚至不起作用。

答案 1 :(得分:4)

对于用户定义的类型,无法在swap中重载namespace std。引言namespace std中的重载(与专业化相反)是未定义的行为(在标准下是非法的,不需要诊断)。

对于template类(而不是template实例而言,通常不能专门使用函数 - 即,std::vector<int>是一个实例,而std::vector<T>是整个template类。似乎是专业化实际上是一种过载。所以第一段适用。

实施用户定义的swap的最佳做法是在与swaptemplate相同的命名空间中引入class函数或重载。

然后,如果在正确的上下文中调用swapusing std::swap; swap(a,b);),这就是在std库中调用它的方式,ADL将会启动,并且会发现您的重载

另一个选项是针对您的特定类型对swap中的std进行完全专业化。这对于template类来说是不可能的(或不切实际的),因为您需要专门针对存在的template类的每个实例。对于其他类,它很脆弱,因为专门化仅适用于特定类型:子类也必须在std中重新专用。

一般来说,功能的专业化非常脆弱,你最好不要引入覆盖。由于您无法在std中引入替代,因此可靠地找到它们的唯一位置是您自己的namespace。您自己的命名空间中的这些覆盖优先于std中的覆盖。

swap注入命名空间有两种方法。两者都是为此目的而工作的:

namespace test {
  struct A {};
  struct B {};
  void swap(A&, A&) { std::cout << "swap(A&,A&)\n"; }
  struct C {
    friend void swap(C&, C&) { std::cout << "swap(C&, C&)\n"; }
  };

  void bob() {
    using std::swap;
    test::A a, b;
    swap(a,b);
    test::B x, y;
    swap(x, y);
    C u, v;
    swap(u, v);
  }
}

void foo() {
  using std::swap;
  test::A a, b;
  swap(a,b);
  test::B x, y;
  swap(x, y);
  test::C u, v;
  swap(u, v);

  test::bob();
}
int main() {
  foo();
  return 0;
}

第一种是将其直接注入namespace,第二种是将其作为内联friend包含在内。 “外部运算符”的内联friend是一种常见模式,基本上意味着您只能通过ADL找到swap,但在此特定上下文中不会增加太多。