9

我正在研究这个关于实现用户定义类型函数的最佳实践微妙问题的迷人答案。(我的问题最初是由讨论向命名空间添加类型的非法性引起的。)swapstd

我不会在这里从上面链接的答案中重新打印代码片段。

相反,我想了解答案。

我在上面链接的答案在第一个代码片段下面,关于重载swapnamespace std(而不是在该命名空间专门化它):

如果你的编译器打印出不同的东西,那么它就没有正确地为模板实现“两阶段查找”。

然后答案继续指出,专业化swapnamespace std而不是重载它)会产生不同的结果(专业化情况下的期望结果)。

但是,答案是在另一种情况下进行的:专门为用户定义的模板交换- 在这种情况下,再次没有达到预期的结果。

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

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

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

  • 专注swapnamespace std用户定义的模板类(如链接答案的最终代码片段)

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

(这将阐明两阶段查找的性质,以及实现用户定义的最佳实践swap的原因;谢谢。)

4

2 回答 2

8

大量标准语的序言

示例中的调用swap()需要一个依赖名称,因为它的参数begin[0]begin[1]依赖于T周围algorithm()函数模板的模板参数。此类从属名称的两阶段名称查找在标准中定义如下:

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

1 对于后缀表达式是从属名称的函数调用,使用通常的查找规则(3.4.1、3.4.2)找到候选函数,除了:

— 对于使用非限定名称查找 (3.4.1) 的查找部分,仅找到来自模板定义上下文的函数声明。

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

不合格的查找定义为

3.4.1 非限定名称查找 [basic.lookup.unqual]

1 在 3.4.1 中列出的所有情况下,按照各自类别中列出的顺序在范围内搜索声明; 一旦找到名称的声明,名称查找就会结束。如果没有找到声明,则程序格式错误。

和参数相关查找(ADL)为

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

1 当函数调用 (5.2.2) 中的后缀表达式是unqualified-id时,可能会搜索在通常的非限定查找 (3.4.1) 期间未考虑的其他命名空间,并且在这些命名空间中,命名空间范围的友元函数或可能会找到不可见的函数模板声明 (11.3)。这些对搜索的修改取决于参数的类型(对于模板模板参数,模板参数的命名空间)。

将标准应用于示例

一个示例调用exp::swap(). 这不是从属名称,不需要两阶段名称查找。因为对 swap 的调用是合格的,所以进行普通查找,只找到通用swap(T&, T&)函数模板。

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

到目前为止,一切都很好。现在再来一次:第三个示例调用swap()并在template<> swap(A&, A&)内部有一个特化namespace exp。查找与第二个示例中的相同,但现在 ADL 不选取模板特化,因为它不在class A. 然而,即使特template<> swap(A&, A&)化在重载解析期间没有发挥作用,它仍然在使用时被实例化。

最后,第四个示例调用并在内部swap()有一个重载,用于生活在全局命名空间中。查找与第三个示例中的相同,并且 ADL 再次没有拾取重载,因为它不在类模板的关联命名空间中。在这种情况下,也没有必须在使用点实例化的特化,所以这里调用了泛型。template<class T> swap(A<T>&, A<T>&)namespace exptemplate<class T> class Aswap(A<T>&, A<T>&)A<T>swap(T&, T&)

结论

即使您不允许向 中添加新的重载namespace std,并且只能添加显式特化,但由于两阶段名称查找的各种复杂性,它甚至无法正常工作。

于 2014-01-27T15:41:31.237 回答
4

用户定义的类型不可能重载swapnamespace std引入重载(与专业化相反)namespace std是未定义的行为(根据标准是非法的,不需要诊断)。

不可能为一个template类专门化一个函数(与template实例相反——即,std::vector<int>是一个实例,std::vector<T>而是整个template类)。看似专业化的东西实际上是一种超载。所以第一段适用。

实现用户定义的最佳实践是在与您的或生活相同的命名空间中swap引入函数或重载。swaptemplateclass

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

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

一般来说,函数的特化是非常脆弱的,最好引入覆盖。由于您无法将覆盖引入std其中,因此可以可靠地找到它们的唯一位置是您自己的namespace. 您自己的命名空间中的此类覆盖也比 in 中的覆盖更std受欢迎。

有两种方法可以将 aswap注入您的命名空间。两者都为此目的工作:

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,第二个是将其包含为 inline friend。“外部运算符”的内联friend是一种常见模式,基本上意味着您只能swap通过 ADL 找到,但在这个特定的上下文中并没有增加太多。

于 2014-01-27T16:28:49.363 回答