Item 25: Consider support for a non-throwing swap.
swap
函数能置换两对象值,功能很重要!
- 异常安全性编程
- 处理自我赋值可能性:赋值运算符需要考虑自我赋值问题
std
的缺省基本实现如下:
1 | namespace std { |
类的 swap
只要类型 T
支持 copying
运算(拷贝构造和拷贝赋值运算)就能使用。 但缺省实现会有多次拷贝,在某些情况下不是性能最好的实现。比如针对 pimpl
手法实现的 class
, 不仅要复制三次 Widget
还需要复制三次 WdigetImpl
, 非常缺乏效率。
1 | class WidgetImpl { |
其实我们发现这种情况只需要将 pImpl
指针交换就好, 我们可以将 std::swap
对 Widget
的特化来实现.
1 | namespace std { |
但上述代码不能通过编译, 因为 pImpl
是私有变量, 所以,Widget
应当提供一个 swap
成员函数或友元函数。 惯例上会提供一个成员函数:
1 | class Widget { |
上述实现与 STL 容器是一致的:提供公有 swap
成员函数, 并特化 std::swap
来调用那个成员函数。
类模板的 swap
如果 Widget
和 WidgetImpl
是 class templates
而非 classes
, 按照上面的 swap
实现方式,你可能会这样写:
1 | template<typename T> |
但上述代码不能通过编译, c++
允许偏特化类模版,却不允许偏特化函数模版(虽然有的编译器中可以编译)。那我们继续尝试重载 std::swap
函数:
1 | namespace std{ |
这里我们重载了 std::swap
,相当于在 std
命名空间添加了一个函数模板。但这在 C++
标准中是不允许的! C++
标准中,客户只能特化 std
中的模板,但不允许在 std
命名空间中添加任何新的模板。 上述代码虽然在有些编译器中可以编译,但会引发未定义的行为,所以不要这么做。所以我们最终可以把 swap
定义在 Widget
所在的命名空间中:
1 | namespace WidgetStuff { |
任何地方在两个 Widget
上调用 swap
时,C++
根据其 argument-dependent lookup
(又称 Koenig lookup
) 会找到 WidgetStuff
命名空间下的具有 Widget
参数的 swap
。
其实类的 swap
也可以在同一命名空间下定义 swap
函数,而不必特化 std::swap
。 但有人可能直接写 std::swap(w1, w2)
,特化 std::swap
可以让你的类更加健壮。
在成员函数中不要直接调用 swap(pImpl, other.pImpl);
因为指定了调用 std::swap
,argument-dependent lookup
便失效了,WidgetStuff::swap
不会得到调用。
如果希望优先调用 WidgetStuff::swap
,如果未定义则取调用 std::swap
,那么应该如何写呢? 看代码:
1 | template<typename T> |
此时,C++
编译器还是会优先调用指定了 T
的 std::swap
,其次是 obj1
的类型 T
所在命名空间下的对应 swap
函数, 最后才会匹配 std::swap
的默认实现。
总结
如何实现 swap
呢?
- 提供一个更加高效的,不抛异常的公有成员函数(比如
Widget::swap
)。 - 在你类(或类模板)的同一命名空间下提供非成员函数
swap
,调用你的成员函数。 - 如果你写的是类而不是类模板,也可以特化
std::swap
,同样地在里面调用你的成员函数。 - 调用时,请首先用
using
使std::swap
可见,然后直接调用swap
。