26.自定义一个不抛异常的swap函数

前言

</br>
swap原先只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自赋值的常见机制。(详见Effective C++ 12)。swap实现的复杂程度极高,本节主要探讨其复杂程度以及应对策略。


swap的具体实现

STL的swap实现

1
2
3
4
5
6
7
8
namespace std{
template<typename T>
void swap(T& a,T& b){
T temp(a);
a=b;
b=temp;
}
}

只要类型T支持copying操作,该swap就一定能正常运行。
该行为必然是正确的,只是对于某些类型而言,其copying行为是不必要的,降低了程序运行的性能。

问题实例

对于”内部仅含指针,指针指向具体数据”的class而言,STL版本的swap函数过于低效。这种类型的常见表现形式是所谓的“pimpl”(pointer to implementation)(详见Effective C++ 32)。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WidgetImpl{//具体实现类
public:
...
private:
int a,b,c;
vector<string> vs;//内部存有大量数据,复制占用时间极大
}
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs){//复制具体实现(其实这里也可以直接复制指针,引用计数)
*pImpl=*(rhs.pImpl);
}
private:
WidgetImpl* pImpl;
}

显然,如果我们需要swap两个widget对象,只需要交换两个指针值。如果使用STL::swap,它不仅复制3个widget,还复制了3个widgetImpl.何以说是三个?

  1. temp(a)
    此时调用拷贝构造,复制了一个Widget,在复制Widget时,其拷贝构造必然构造了一个WidgetImpl
  2. a=b
    调用Widget::operator=,复制了一个WidgetImpl
  3. b=temp
    同上

解决思路

我们希望告诉std::swap,置换widget只需要置换内部指针。无疑我们需要将std::swap针对Widget特例化,以下是无法通过编译的基本构思:

1
2
3
4
5
6
namespace std
template<>
void swap<Widget>(Widget&a,Widget&b){
swap(a.Impl,b.Impl);
}
}

在一般情况下不允许改变std命名空间的任何东西,但是这里只是添加了某个特例化版本,属于安全行为。(Effective STL 42 也这样做了)
之所以无法编译是因为我们试图去访问private成员。使用friend函数当然是可耻的:封装性被破坏。

解决方案

定义一个member function做真正的swap工作,然后再令STL的swap函数调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget{
public:
void swap(Widget& other){
using std::swap;//必须声明
swap(pImpl.other.pImpl);
}
}
namesapce std{
template<>
void swap<Widget>(Widget& a,Widget& b){
a.swap(b);
}
}

这种写法不仅能够通过编译,并且还与stl容器兼容,因为stl容器也提供了public swap成员函数与std::swap特例化版本。

template下的swap实现

无法完成function template偏特化

假定现在Widget与WidgetImpl都是template class:

1
2
3
4
template<typenmae T>
class WidgetImpl{...};
template<typename T>
class Widget{...};

此时在Widget::swap照旧,但试图特化std::swap时却遇到了麻烦:
1
2
3
4
5
6
namespace std{//无法编译
template<typenmae T>
void swap<Widget<T> >(Widget<T>& a,Widget<T>& b){
a.swap(b);
}
}

不能编译的理由是:我们企图偏特化(partially specialize)一个function template(std::swap),但C++只允许对class template执行偏特化。(所谓偏特化就是针对某个部分执行特例化,但特例化之后得到的却仍然是一个模板而非实例,在本例中,swap本身是一个function template,我们试图针对其参数为Widget<T>时特例化,但特例化得到的仍然是一个function template,而非像上文那样的实例函数。关于偏特化的介绍,详见C++ Primer P628)

std命名空间禁止添加新的重载template

如果你需要偏特化一个template function,那么常规做法为其添加一个重载版本:

1
2
3
4
5
6
namespace std{//这也是不合法的
template<typename T>
void swap(Widget<T>& a,Widget<T>& b){
a.swap(b);
}
}

对于std命名空间,客户可以全特化其中的template,但不允许添加任何新的template进入,上述代码属于新的swap。

声明non-member swap

我们将定义一个non-member swap函数,由它调用member swap,但不再将non-member swap声明为std::swap的特例化,为了简化说明,我们将一切与Widget相关的机能都置入WidgetStuff命名空间:

1
2
3
4
5
6
7
8
namespace WidgetStuff{
template<typename T>
class Widget {...}
template<typenmae T>
void swap(Widget<T>& a,Widget<T> &b){
a.swap(b);
}
}

如此一来,根据C++的名称查找法则,每当试图swap Widgte对象时,都会第一时间找到我们所定义的non-member版本,这并非令客户失去了定制操作的能力。只要加上using std::swap,那么他们的swap又会回归STL版本,这也是我们的member swap使用using的原因。


swap与异常

</br>
member swap禁止抛出异常,但非成员版允许抛出异常,因为默认swap以copying函数为基础,而copying行为允许抛出异常。
自定义swap意味着高效交换,并且不抛出异常。这两个特性其实彼此相关:高效交换的基础是基于内置类型的操作,而内置类型的操作绝不会抛出异常。


总结

  1. 当std::swap对class效率不高时,提供一个swap成员函数并且确定其noexcept
  2. 如果你提供了member swap函数,也应该提供一个non-member来调用前者。对于classes(并非templates),也请特例化std::swap
  3. 调用swap时应该先声明使用的是哪个命名空间内的swap
  4. std命名空间允许全特例化,但禁止在其内部加入一些对std而言全新的东西