一、标准模板库中的swap函数
- 标准模板库中的swap函数,其是STL中的一部分,后来成为异常安全性编程(见条款29)以及用来处理自我赋值可能性(见条款11)的一个常见机制
- 下面是STL中swap的源码:只要类型T支持拷贝(拷贝构造函数或者拷贝赋值运算符),缺省的swap函数就可能帮你对两个类型为T的对象进行兑换
- 为什么不使用std::swap()函数:
- 从源码可以看出,std::swap函数,其涉及三个对象的复制操作:a复制到temp,b复制到a,temp复制到b
- 但是对于我们自己设计的类来说,可能有时用不到这些复制操作,复制太多,效率太低
- 因此对于自己设计的类(非模板类,或模板类),都不想使用std提供的默认swap()版本。本文下面将会介绍针对于自己设计的“非模板类,或模板类”设计自己的swap()函数
二、针对于非模板类,设计全特化的std::swap()
①有些情况下我们不希望使用std::swap()
- 例如我们有下面的一个类WidgetImpl,其中保存Widget的数据:
//针对Widget数据设计的class class WidgetImpl { public: //... private: int a, b, c; std::vector<double> v; //...其他数据 };
- 此时又有一个类Widget,其采用pimpl手法(参阅条款31),里面保存一个指针指向于一个对象(此处为WidgetImpl),该对象内含真正的数据:
//这个class使用pimpl手法 class Widget { public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs)//拷贝赋值运算符 { //... *pImpl = *(rhs.pImpl); //... } private: WidgetImpl* pImpl; //此处是指针 };
为什么我们不希望使用std::swap():
- 我们使用时真正用到的是Widget对象,如果我们使用std::swap函数,那么其会进行3次Widget对象的复制,并且每次复制的时候调用的是上面Widget类中的operator=运算符
- 因此std::swap函数会调用三次operator=函数,这种效率是非常低的
- 可以看出Widget中只用一个pImpl指针保存数据,因此我们想要交换两个Widget对象,只要交换它们两个的pImpl指针就行了,这样就达到数据互换的目的了(实现见下面的②)
②使用全特化的std::swap()函数
- 我们希望让std::swap()知道,当Widget类进行替换的时候只需要替换其内部的pImpl指针就可以了,此时我们可以针对std::swap()做一个全特化的版本
- 代码如下:
//此swap用于表示这个函数针对“T是Widget”而设计的 namespace std { //template<>用于表示这是一个std::Swap的全特化版本 template<> void swap<Widget>(Widget& a, Widget& b) { //错误的,pImpl是private的,无法编译通过 swap(a.pImpl, b.pImpl); } }
- 但是上面的代码是无法编译通过的,因为pImpl是private的,因此函数无法编译通过
③为类设计一个swap成员函数,并设计一个全局swap函数
- 通过②的设计我们知道,由于pImpl是private的,因此全特化的swap无法编译通过。我们有两种解决办法:
- 1.将上面的特化版本定义为class的friend,但是我们不希望这么做
- 2.(此处要介绍的)为class设计一个swap()函数,在全特化的版本中调用我们class的swap()成员函数
- 此时我们可以修改Widget class,使全特化的版本可以应用于我们的Widgte class:
class Widget { public: void swap(Widget& rhs) { using std::swap; //为什么要有这个声明,见下 swap(pImpl, rhs.pImpl); //调用std::swap()函数,只交换两个对象的指针 } private: WidgetImpl* pImpl; }; namespace std { template<> void swap<Widget>(Widget& a, Widget& b) { a.swap(b); //调用Widget::swap()成员函数 } }
- 现在的代码可以编译通过了,并且可以使用我们的std::swap()全特化版本了
- STL容器中的swap()成员函数就是按照这种原理实现的:为class提供public swap()成员函数和std::swap()的全特化版本(然后以后者调用前者)
- 顺便提一下:上面我们是在std中偏特化了std::swap()函数,但是我们不建议这样做,因为这样可能会对std命名空间造成污染,一种解决解决方法是在std命名空间之外定义全特化swap()函数(下面的“三”中有演示案例)
三、如果是模板类,那么该如何解决?
- 紧接着“二”,如果Widget和WidgetImpl都不是普通的类,而是模板类。如下所示:
//此时这两个类都变成模板类
template<typename T>
class WidgetImpl { };
template<typename T>
class Widget { };
偏特化是错误的
- 对于“二”中的解法,我们可能会设计下面的代码(但是是错误的,无法编译通过):
- 我们的Widget和WidgetImpl都是类模板,因此我们尝试偏特化std::swap()函数
- 但是由于C++只允许对类模板偏特化,不允许对函数模板偏特化,所以下面的代码是无法编译通过的
template<typename T> class WidgetImpl{ //同上 }; template<typename T> class Widget{ //同上 }; namespace std { //此处是错误的,C++只允许对类模板偏特化,不允许对函数模板偏特化 template<typename T> void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
使用重载版本代替偏特化
- 上面我们尝试偏特化函数模板,但是C++编译器不允许。因此为了解决这种问题,我们可以为其添加一个重载版本
- 代码如下:
template<typename T> class WidgetImpl{ //同上 }; template<typename T> class Widget{ //同上 }; namespace std{ //这里std::swap的一个重载版本,而不是特化版本 template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
在自己的命名空间中定义swap()函数
- 上面我们重载了std::swap函数,但是是在std命名空间中进行重载的,std是个特殊的命名空间,其管理规则比较特殊,因此我们不建议在std中重载任何内容
- 一种解决方法就是在自己的命名空间中定义swap()函数。代码如下:
//假设这是自己设计的命名空间 namespace WidgetStuff{ template<typename T> class WidgetImpl{ //同上 }; template<typename T> class Widget{ //同上 }; template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
- 注意事项:
- 我们在自己的命名空间中定义的swap()函数不属于std::swap()的重载版本,因为它们作用域不一致
- 此处我们以命名空间为例,其实不使用命名空间也可以,我们主要是为了指出不要与std::swap()产生冲突。但是为了让全局数据空间太过杂乱,我们建议使用命名空间
- 在“二”中最后我们提到过,不要在std中进行全特化。因此此处的方法不仅适用于模板类,同样也适用于非模板类
四、使用using声明
- 假设此时我们编写了一个函数模板,其接受两个参数,并在模板内调换两个元素。代码如下:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
//...
swap(obj1,obj2); //调用哪一个版本的swap()函数哪?
//...
}
- 根据C++的名称查找规则,对于swap()的调用会根据以下顺序查找:
- 先在doSomething()函数所在的命名空间中,查找是否有针对于类型T所特化的swap()函数。如果有就调用,如果没有进行下一步
- 调用std::swap()函数调换两个元素
- 通过上面我们知道:
- 在调用swap()时,先在自己的命名空间中查找是否有适当的特化版本的swap()函数,如果有就调用,如果没有就调用std::swap()
- 因此,为了使std::swap()函数在函数内可用,我们通常使用using声明,导入std::swap()函数。让其在没有特化版本的swap()时去调用std::swap()
template<typename T>
void doSomething(T& obj1,T& obj2)
{
//使std::swap在次函数内可用
//如果没有针对于T的特化swap版本,那么就调用std::swap
using std::swap;
//...
swap(obj1,obj2);
//...
}
- 一个注意事项:
- 不要在调用swap()函数的时候指定std::限定符,否则将永远无法调用自己的特化版本
- 例如下面的代码:强制调用std::swap(),那么自己定义的特化swap()将永远不会被调用
template<typename T>
void doSomething(T& obj1,T& obj2)
{
//...
std::swap(obj1,obj2); //错误的swap调用方式
//...
}
五、swap()函数的总结
1)如果std::swap()函数对你的类或类模板使用,并且不会影响效率,那么就优先使用std::swap()
2)如果std::swap()针对于你的类或类模板不太使用,那么就自己设计swap()函数。设计如下:
- 在类中提供一个public swap()函数,在其中置换两者的数据(代码自己设计,但是不允许抛出异常)
- 然后在你的类或类模板所在的命名空间中提供一个非成员函数swap(),然后在其中调用成员函数swap()
- 如果你的类(而不是类模板),那么可以为你的类特化std::swap(),并在特化版本的swap()中调用成员函数swap()
六、swap成员函数不能抛出异常
- 成员版的swap函数绝不可能抛出异常。因为swap的一个最好的应用是帮助类(和类模板)提供强烈的异常安全性保障(条款29介绍)
- 但此技术基于一个假设:成员版的swap绝不抛出异常。这一约束只施加于swap成员版本,不可施加于非成员版本,因为swap缺省版本是以拷贝构造函数和拷贝赋值运算符为基础,而一般情况下两者都允许抛出异常
- 因此当你写下一个自定义的swap,往往提供的不只是高效置换对象的版本,而且还不抛出异常
七、本文总结
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非template class),也请特化std::swap
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
- 用“用户定义类型”进行std template全特化时最好的,但千万不要尝试在std内加入某些对std而言全新的东西