一、标准模板库中的swap函数
- 标准模板库中的swap函数,其是STL中的一部分,后来成为异常安全性编程(见条款29)以及用来处理自我赋值可能性(见条款11)的一个常见机制
- 下面是STL中swap的源码:
- 只要类型T支持拷贝(拷贝构造函数或者拷贝赋值运算符),缺省的swap函数就可能帮你对两个类型为T的对象进行兑换
![](https://img-blog.csdnimg.cn/20200216133900426.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxNDUzMjg1,size_16,color_FFFFFF,t_70)
二、设计自己的swap函数
- 为什么不使用std::swap函数:
- 从源码可以看出,std::swap函数,其涉及三个对象的复制操作:a复制到temp,b复制到a,temp复制到b
- 但是对于我们自己设计的类来说,可能有时用不到这些复制操作,复制太多,效率太低
有些情况下不要使用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;
};
- 我们真正使用时使用的的是Widget对象,如果我们使用std::swap函数,那么其会进行3次Widget对象的复制,并且每次复制的时候调用的是上面Widget类中的operator=运算符。因此std::swap函数会调用三次operator=函数,这种效率是非常低的
使用全特化的swap函数
- 我们希望让std::swap知道,当Widget类进行替换的时候只需要替换其内部的pImpl指针就可以了,此时我们可以针对std::swap做一个全特化的版本。如下:
//template<>用于表示这是一个std::Swap的全特化版本
//此swap用于表示这个函数针对“T是Widget”而设计的
namespace std {
template<>
void swap<Widget>(Widget& a, Widget&b)
{
swap(a.pImpl, b.pImpl);
}
}
- 但是上面的代码是无法编译通过的,因为pImpl是private的,因此函数无法编译通过
为类设计一个swap成员函数,并设计一个全局swap函数
- 由于pImpl是private的,因此上面的全特化的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函数
}
}
- 现在的代码可以编译通过了,并且可以使用我们全特化的swap版本了
三、偏特化遇到的问题
- 如果Widget和WidgetImpl都是模板类,而不是普通的类,如下所示:
template<typename T>
class WidgetImpl { };
template<typename T>
class Widget { };
- 此时如果我们我们还想偏特化一个针对于Widget的swap函数,那么就会出出错,因为:
- 我们企图偏特化一个function template(std::swap),但C++只允许对class template偏特化,因此这里是错误的
namespace std{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
- 因此如果我们想偏特化一个function template,那么就需要为其添加一个重载版本的swap函数
namespace std{
//这里std::swap的一个重载版本
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
四、放弃重载版本的swap函数
- 上面我们重载了std::swap函数,但是std是个特殊的命名空间,其管理规则比较特殊,客户可以全特化std内的template,但是不可以添加新的template到std里面
- 因此我们我们应该将偏特化版本的swap函数置于我们自己的命名空间中
namespace WidgetStuff{ //自己命名的命名空间
//...
template<typename T>
class Widget { };
//...
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
- 现在我们在任何地方调用swap()函数转换两个Widget对象,那么上面的swap函数就会被调用
五、使用using声明
- 上面的做法对于class或class template都行的通,但不幸的是你应该为class特化std::swap(下面会介绍)
- 如果你想让你的“class专属版”swap在尽可能多的环境下被调用,你需要同时在该calss所在命名空间内写一个non-member版本以及一个std::swap全特化版本
- 另外,其实我们也可以不使用上面的命名空间,这些规则也可以生效
- 如果我们正在写一个function template,其内需要调用两个对象值:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
//...
swap(obj1,obj2);
//...
}
- 上面的swap应该调用哪一个版本呢?是可能存在的特化版?还是存在T专属版本而且还可栖息于某个命名空间内?你希望的应该是调用T专属八本,并且该版本不存在的情况下调用std内的一般化版本。此时修改代码如下:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
using std::swap; //使std::swap在次函数内可用
//...
swap(obj1,obj2);
//...
}
- 一旦编译器看到对swap函数的调用,它会查找适当的swap函数并调用
- 需要注意,下面添加额外的修饰符是错误的,因为强迫编译器调用swap函数,而永远都不会调用专属于T的版本了
template<typename T>
void doSomething(T& obj1,T& obj2)
{
//...
std::swap(obj1,obj2); //错误的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而言全新的东西