条款 25 :考虑写出一个不抛出异常的swap函数
Consider support for a non-throwing swap.
- 所谓swap(置换)两对象值,意思是将两对象的值彼此赋予对方。标准库缺省swap函数典型实现如下:
namespace std{
template<typename T>
void swap(T& a,T& b){
T temp(a);
a=b;
b=temp;
}
}
在上述代码中只要类型T支持copying,缺省的swap实现代码会自动完成对象的置换。观察上述代码,其涉及了三个对象的复制,但是细想,对于一些类型这些复制是没有必要的。若是非要这么写,无疑是降低了速度。其中最主要的形式就是”以指针指向一个对象,内含真正数据“即所谓的"pimpl手法"(pointer to implementation)我们来看下列例子:
class WidgetImpl{//针对Widget数据而设计的class
public://细节不重要
...
private:
int a,b,c;
std::vector<double> v;//可能有很多数据,意味着复制时间长
...
};
class Widget{//这个class使用pimpl手法
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs){
...//复制Widget时,令其复制其WidgetImpl对象
*pImpl=*(rhs.pIml);
...
}
...
private:
WidgetImpl* pImpl;//指针,所值对象内含Widget数据
};
使用这个类我们一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但是问题是缺省的swap函数是不知道这一点的。明确告诉swap的具体实践是将std::swap对Widget特化。下面是基本思想:
namespace std{//这是针对std::swap的T是Widget的特化版本
template<>//改代码目前还不能通过编译
void swap<Widget>(Widget& a,Widget& b){
swap(a.pImpl,b.pImpl);//置换时只置换指针
}
}
上述函数一开始的template<>表示他是std::swap的一个全特化版本,函数之后的< Widget >表示这个特化版本是针对T是Widget而设计的。换一句话说就是:当一般性的swap用在Widget身上时就启用这个版本。通常情况下我们不能够(不被允许)更改std内的任何的任何内容,但是可以被允许为标准template制造特化版本。
我们说过上述代码还无法通过编译,原因是pImpl只private,而swap却妄想直接利用。解决之道就是,令特化的swap成为其friend.这里的声明方式与以往不太相同,我们的做法是:令Widget声明一个名为swap的public成员函数来做真正的置换工作,然后将std::swap特化,令他调用该函数成员:
class Widget{
public:
...
void swap(Widget& other){
using std::swap;//这个声明是有必要的,说明该域内的swap为std::swap
swap(pImpl,other.pImpl);//若要置换,就置换指针
}
...
};
namespace std{//修订后的特化版本
template<>
void swap<Widget>(Widget& a,Widget& b){
a.swap(b);//若要置换,调用其swap成员函数
}
}
该做法不仅能通过编译,还与STL行为一致了。(具体不细说了)
然而当Widget和WidgetImpl都是calsses template而不是calsses时,我们也可以对WidgetImpl内的数据类型加以参数化:
template<typename T>
class WidgetImpl{...};
template<typename T>
class Widget{...};
想想上述情况我们的特化swap怎么写,看看下述代码:
namespace std{
template<typename>
void swap<Widget<T>>(Widget<T>& a,Widget<T>& b){//不合法
a.swap(b);//若要置换,调用其swap成员函数
}
}
这看起来是合乎情理的,但是不合法。原因是:我们企图偏特化一份function template,但是C++只允许对calsses template偏特化,在function template上偏特化是行不通的。
当你打算偏特化一个函数模板,常用方法是为其添加一个重载版本,看下述代码:
namespace std{
template<typename>
void swap(Widget<T>& a,Widget<T>& b){//这也不合法
a.swap(b);//若要置换,调用其swap成员函数
}
}
一般而言重载函数模板是没有问题的,但是std标准由C++标准成员会给出,他不允许添加你自己的function template版本。 所以上述代码依旧行不通。
为了解决这个问题,我们的做法是:我们声明一个non-member swap让他调用member swap,但这个non-member swap不再是std::swap的特化版本或重载版本。来看下列代码:
namespace WidgeStuff{
...
template<typename T>
class Widget{...};
...
void swap(Widget<T>& a,Widget<T>& b){//这里并不属于std了
a.swap(b);//若要置换,调用其swap成员函数
}
那么现在调用置换时,就会调用该空间的swap而不会影响std。
请记住
1. 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
2. 如果你提供一个member swap,也应该提供一个non-member swap来调用前者。对于calsses(而非template),也请特化std::swap.
3. 调用swap时应该针对std::swap使用using 声明,然后调用swap并且不带任何”命名空间修饰资格“。
4. 为用户定义类型进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。