条款25:考虑写出一个不抛出异常的swap函数
swap函数是异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。
缺省情况下,swap动作可由标准程序库提供的swap算法完成。(对象需要支持copying函数)
namespace std{
template <typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
这种方法效率较低,需要将对象进行三次复制运算,每次运算都复制里面的每个成员变量。改进方法:采用“pimpl方法”(pointer to implementation),定义一个专门封装真正数据的类,然后原有类别包含一个指针指向该数据类。class WidgetImpl { //针对Widget数据设计的class
public:
....
private:
int a,b,c; //包含Widget的数据
std::vector<double> v;
.....
};
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) {
...
*pImpl = *rhs.pImpl;//进行深度拷贝,将指针内的数据进行复制
...
}
...
private:
WidgetImpl* pImpl;
};
一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但是缺省的swap算法不知道这一点。它仍然复制pImpl所指对象的所有内容,所以我们应该将std::swap针对Widget特化,但是因为swap函数要访问Widget里面的private成员,因此我们可以将这个特化版本声明为friend,但是这个版本的特化和以往的规则不同:在类中声明一个public成员函数做真正的置换工作,然后将std::swap特化,让其调用该成员函数(这种做法不仅可以实现功能,还可以跟STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本):
class Widget {
public:
...
void swap(Widget& rhs)
{
using std::swap; //这个声明的必要性稍后解释
swap(pImpl, rhs.pImpl);
}
...
};
namespace std {
template <>
void swap<Widget> (Widget& a, Widget& b) //特化版本
{
a.swap(b);
}
}
template <typename T>
class WidgetImpl {....};
template <typename T>
class Widget {.......};
如果像以往一样进行特化会生成对函数的偏特化。
namespace std {
template<typename T>
void swap< Widget<T> >(Widget<T>& a, Widget<T>& b)
{ a.swap(b);}
}
C++只允许对class templates偏特化,在function templates上偏特化是行不通的。(当打算偏特化一个函数模版时,常用做法是简单的为他添加一个重载模版版本)
namespace std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{ a.swap(b);}
}
但是,std是个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的templates,但不可以添加新的templates(classes或者functions或其他任何东西)到std里面。因此,我们的做法是我们还是声明一个non-member swap让它调用member swap,但不再将那个non-member swap声明为std::swap的特化版本或者重载版本,而是将其放在另一个命名空间内。
namespace WidgetStuff {
...
template<typename T>
class Widget {...};
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
现在,任何地点的任何代码如果打算置换两个Widget对象,因而调用swap,C++的名称查找法则会找到WidgetStuff内的Widget专属版本。
如果你想让你的“class专属版”swap在尽可能多的语境下被调用,你需要同时在该class所在的命名空间内些一个non-member版本以及一个std::swap特化版本(为了让std::swap特化版本被发现,所以应该如前述代码一样加上using std::swap)。
总结:首先,如果swap的缺省实现码对你的class或者class template提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会去的缺省版本,而那将会有良好的运作。
其次,如果swap缺省实现版的效率不足(那几乎总是意味着你的class或者template使用了某种pimpl手法),试着做以下事情:
1.提供一个public swap成员函数,让你高效地置换你的类型的两个对象值。2.在你的class或者template所在的命名空间内提供一个non-member swap,并令其调用swap成员函数。3.如果正编写一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数。
最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸的调用swap。
成员版swap绝不可抛出异常。因为swap的一个最好的应用是帮助classes提供强烈的异常安全性保障。这一技术基于一个基础:成员版swap绝不抛出异常。