我正在研究this fascinating answer到subtle 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
的原因;谢谢。)
答案 0 :(得分:8)
示例中对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
的最佳做法是在与swap
或template
相同的命名空间中引入class
函数或重载。
然后,如果在正确的上下文中调用swap
(using 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
,但在此特定上下文中不会增加太多。