Item 25: Consider support for a non-throwing swap.
Swap函数最初由STL引入,已经成为异常安全编程(见Item 29)的关键函数, 同时也是解决自赋值问题(参见Item 11:赋值运算符的自赋值问题)的通用机制。 std
中它的基本实现是很直观的:
namespace std{
template<typename T>
void swap(T& a, T& b){
T tmp(a);
a = b;
b = tmp;
}
}
可以看到,上述Swap是通过赋值和拷贝构造实现的。所以std::swap
并未提供异常安全, 但由于Swap操作的重要性,我们应当为自定义的类实现异常安全的Swap,这便是本节的重点所在。
先不提异常安全,有时std::swap
并不高效(对自定义类型而言)。 比如采用 pimpl idiom(见Item 31)设计的类中,只需要交换实现对象的指针即可:
class WidgetImpl;
class Widget { // pimpl idiom 的一个类
WidgetImpl *pImpl; // 指向Widget的实现(数据)
public:
Widget(const Widget& rhs);
};
namespace std {
template<> // 模板参数为空,表明这是一个全特化
void swap<Widget>(Widget& a, Widget& b){
swap(a.pImpl, b.pImpl); // 只需交换它们实体类的指针
}
}
上述代码是不能编译的,因为pImpl
是私有成员!所以,Widget
应当提供一个swap
成员函数或友元函数。 惯例上会提供一个成员函数:
class Widget {
public:
void swap(Widget& other){
using std::swap; // 为何要这样?请看下文
swap(pImpl, other.pImpl);
}
};
接着我们继续特化std::swap
,在这个通用的Swap中调用那个成员函数:
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b){
a.swap(b); // 调用成员函数
}
}
到此为止,我们得到了完美的swap
代码。上述实现与STL容器是一致的:提供共有swap
成员函数, 并特化std::swap
来调用那个成员函数。
当Widget
是类模板时,情况会更加复杂。按照上面的Swap实现方式,你可能会这样写:
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
namespace std {
template<typename T>
// swap后的尖括号表示这是一个特化,而非重载。
// swap<>中的类型列表为template<>中的类型列表的一个特例。
void swap<Widget<T> >(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
悲剧的是,上述代码不能通过编译。C++允许偏特化类模板,却不允许偏特化函数模板(虽然在有些编译器中可以编译)。 所以我们干脆不偏特化了,我们来重载std::swap
函数模板:
namespace std {
template<typename T>
// 注意swap后面没有尖括号,这是一个新的模板函数。
// 由于当前命名空间已经有同名函数了,所以算函数重载。
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
这里我们重载了std::swap
,相当于在std
命名空间添加了一个函数模板。这在C++标准中是不允许的! C++标准中,客户只能特化std
中的模板,但不允许在std
命名空间中添加任何新的模板。 上述代码虽然在有些编译器中可以编译,但会引发未定义的行为,所以不要这么搞!
那怎么搞?办法也很简单,就是别在std
下添加swap
函数了,把swap
定义在Widget
所在的命名空间中:
namespace WidgetStuff {
template<typename T>
class Widget { ... };
template<typename T>
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
任何地方在两个Widget
上调用swap
时,C++根据其argument-dependent lookup(又称 Koenig lookup) 会找到WidgetStuff
命名空间下的具有Widget
参数的swap
。
那么似乎类的Swap也只需要在同一命名空间下定义swap
函数,而不必特化std::swap
。 但是!有人喜欢直接写std::swap(w1, w2)
,特化std::swap
可以让你的类更加健壮。
因为指定了调用
std::swap
,argument-dependent lookup 便失效了,WidgetStuff::swap
不会得到调用。
说到这里,你可能会问如果我希望优先调用WidgetStuff::swap
,如果未定义则取调用std::swap
,那么应该如何写呢? 看代码:
template<typename T>
void doSomething(T& obj1, T& obj2){
using std::swap; // 使得`std::swap`在该作用域内可见
swap(obj1, obj2); // 现在,编译器会帮你选最好的Swap
}
此时,C++编译器还是会优先调用指定了T的std::swap
,其次是obj1
的类型T
所在命名空间下的对应swap
函数, 最后才会匹配std::swap
的默认实现。
如何实现Swap呢?总结一下:
Widget::swap
)。swap
,调用你的成员函数。std::swap
,同样应当调用你的成员函数。using
使std::swap
可见,然后直接调用swap
。除非注明,本博客文章均为原创,转载请以链接形式标明本文地址: http://harttle.com/2015/08/23/effective-cpp-25.html