首先本篇博客的主要思想是:系统自带的swap函数有时候不能满足我们的需求,所以在一些情况下我们就需要自己去写swap函数。此条款的主要内容就是告诉你该如何去写你要的swap函数,下面开始正文来好好地介绍一下此条款的内容
1.首先来看一下库里面给的swap函数的原型
可以看出来标准程序库提供的swap函数是这样的实现的,通过拷贝和赋值来实现。可以看出来他也没有提供异常安全,所以在我们自己写的swap函数中一定要注意异常安全这个问题。因为库中给的swap函数对于简单内置类型的置换还是可以的,但是对于一些稍微复杂一点的了,类类型的话就会显得效率比较低下。比如下面这种情况
class WidgetImpl;//类
class Widget
{
WidgetImpl *pImpl; // 指向Widget的实现(数据)
public:
Widget(const Widget& rhs);
};
如果我们要交换两个Widget对象的话,库中的swap就会显得效率很低,因为要复制三个Widget对象,如果WidgetImpl类中的成员也是自定义类型的话,呢么就会复制更多的东西,造成效率低下。上面的这个例子我们可以很明显得看出来。我们只需要交换内部的指针就可以了,没必要复制那么多次无用功的对象。所以我们可以想到
特化库中的swap函数,代码如下:
namespace std
{
template<>
void swap<Widget>(Widget& a, Widget& b){
swap(a.pImpl, b.pImpl);
}
}
这样我们企图如果参数是Widget的时候可以直接条用全特化的函数,从而实现将指针传过去进行交换。可是这个函数可能无法通过编译,因为Widget中的指针成员pImpl是私有的。所以我们应该想办法解决这个问题,那就是提供一个成员函数或者友元函数使其可以访问指针成员不就可以了嘛,在这里我们采取成员函数(这里使用特化版本来调用swap函数)。代码如下:
class Widget {
public:
void swap(Widget& other){
using std::swap;
swap(pImpl, other.pImpl);
}
};
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b){
a.swap(b); // 调用成员函数
}
}
在这里就可以通过特化版的函数来调用成员函数,从而使成员函数访问成员将其私有成员函数作为实参调用swap函数从而达到我们想要的效果。
这样写不但能通过编译而且和STL容器有一致性,因为所有的STL容器也都有public的swap函数和特化库中的swap函数(调用Public swap函数)。
注意:上面为什么写了个using std::swap呢?因为这句话其实就是强调没有函数使用的情况下可以调用库中的swap函数,本意就是提醒编译器别忘记了库中的函数。后面再做详细的 介绍。
上面阐述的问题都是类类型的问题,可以按照我们所说得解决问题,但是如果是类模板类型的话呢?
template<class T>
class Widget {};
template<class T>
class WidgetImpl{};
现在上面的两个类都是模板类型的了。
我们可能会想到这么改造一下函数的特化就可以了
namespace std {
template<>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b){
a.swap(b); // 调用成员函数
}
}
可是不行呀,因为C++规定函数只能是全特化,而上面的这种形式就是偏特化了,所以行不通。。。。所以如果你打算偏特化一个函数的时候,可以给这个函数添加重载函数,
比如这个样子就是std::swap函数的重载了
namespace std
{
template<class T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
可是事与愿违,尽管这样是个好方法,但是C++规定又不允许了,因为std是一个特殊的命名空间,客户可以特化里面的模板,但是不可以添加新的模板类或者模板函数到std里面去,std的内容完全由C++标准委员会决定,委员会禁止我们添加已经声明好了的东西,虽然这种违禁行为可能会被编译和执行,但是这是不明确的行为,所以不要给std添加新东西,所以此路不同。。。不同那我们也得想办法解决呀,
解决方法就是声明一个非成员swap函数,但不将其声明为std中swap的特化版本了,为了方便和更好的说明一些问题我们将其都定义在一个新的命名空间中
namespace WidgetStuff
{
template<typename T>
class Widget
{
........其余省略
public:
void swap(Widget& other){
using std::swap;
swap(pImpl, other.pImpl);
}
};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
现在这样的话任何地点任何代码如果打算交换Wifget对象的话,都会调用WidgetStuff的swap函数。
这种写法对于类类型和类模板类型都行的通,但是这并不是最完美的,因为如果你想让你的类专属的swap函数在更可能多的语境下呗调用的话,我们应该同时在这个类所在的命名空间中写一个非成员函数和std的特化版本。
这样的话如果
一旦编译器看到对swap的调用,他们便查找适当的swap函数并调用,C++的名称查找法则确保将找到全局作用域或T所在之命名空间内的任何T专属的swap。如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参查找规则”找出WidgetStuff命名空间中的swap,如果没有T专属的swap存在,编译器就是用库中的swap函数,这就取决于using std::swap这句话的作用。而然编译器还是比较喜欢特化版本的swap如,而非一般化的template。
致此大致的swap函数就说完了。
这里有三个要求每个都好好理解
如果你觉得库的swap的效率不够(意味着你使用了指针成员的这种用法)
1.提供一个public swap成员函数,让他来调用swap来置换你的指针的值
2.在你的类或者模板所在的命名空间内提供一个非成员函数swap,让他来调用成员函数
3.如果你正在编写一个类而不是模板类的话,记得特化swap,并让他来调用你的swap成员函数
第一个准确的说是2 3 的基础,因为2 3 所调用的swap成员函数就是1中的
最后一点要强调的就是不要在你写的成员swap函数中抛出异常,因为你的这个函数是最终的最简单的版本来调用库函数的,所以效率最高这也往往意味着调用的库函数的实参都是内置类型的,而内置类型的操作绝对不会抛出异常的!