目录
std::swap
- swap是一个很常见的函数,用来交换两个变量的值。而且swap也可以用来处理赋值函数中的异常安全问题。当然,这取决于swap函数本身不会抛出异常。
- swap函数的实现通常是这样的:
template <class T>
void swap(T& a, T& b){
T temp(a);
a = b;
b = temp;
}
- 我们称这种版本的swap为default版本的swap。
- 显然的,swap调用了一次拷贝构造函数和2次operator=函数。
- 在某些情况下(T是内置类型,如int),default的swap的效率很高。但是,对于某些类,这些动作无一必要。
class MyClassNode {
private:
vector<int> _pV; //这个vector会带有很多数据,
int _num; //拷贝or赋值浪费大量资源。
public:
//实现省略
};
class MyClass {
private:
MyClassNode* _pNode;
public:
MyClass& operator=(const MyClass& value) {
//...
*_pNode = *value._pNode; //调用MyClassNode的赋值,最终会调用vector的operator=;
//...
}
};
- 如果我们尝试swap两个MyClass的对象,那么默认的swap函数会做一次拷贝构造和两个operator=。它不仅仅调用MyClass的拷贝构造和赋值,它还会调用MyClassNode的,进而会调用vector的。这是很大的效率损失!
- 而实际上,我们只需要交换MyClass中的两个_pNode指针就足矣。
- 我们可以对std::swap写入一个特化版本的swap,像这样,
namespace std {
template<>
void swap<MyClass>(MyClass& lhs, MyClass& rhs)noexcept {
swap(lhs._pNode, rhs._pNode);
}
}
- 上面是一个针对MyClass类的swap函数的全特化版本。
- 但是,这样的swap不能够通过编译。答案很简单,_pNode是MyClass的私有成员,无法被外部的swap访问。
- 我们可以将该特化的swap声明为MyClass的friend函数,这是一种类外访问类内私有成员的方法。我们这里不这样做,我们采用另外一种常用的方法(ps: 这种方法很常用,也很重要!!):
- 我们在MyClass类内部写一个member版本的swap(声明为public),让这个member swap去实现真正的"交换",然后用特化版本的swap去调用该swap。
在类内实现swap,然后特化std::swap调用之
class MyClass {
public:
//省略部分与上面一样,只是增加了swap函数;
void swap(MyClass& value) {
using std::swap; //这行代码是必要的;
swap(_pNode, value._pNode);
}
};
namespace std {
template<>
void swap<MyClass>(MyClass& lhs, MyClass& rhs)noexcept {
lhs.swap(rhs);
}
}
- 这种写法可以帮助我们很好的交换MyClass对象,而以较小的代价。而且这种写法与STL保持一致性,他们也是这么干的。
- 你可能注意到,在member的swap版本中,我增加了一行代码using std::swap,后面会说这句代码的必要性。
- 但是,如果你的class是template class呢?即,如果是一个类模板,swap该如何写呢?
template <class T>
class MyClass {
private:
MyClassNode<T>* _pNode;
public:
MyClass<T>& operator=(const MyClass<T>& value) {
//...
*_pNode = *value._pNode; //调用MyClassNode的赋值,最终会调用vector的operator=;
//...
}
void swap(MyClass<T>& value) {
using std::swap;
swap(_pNode, value._pNode);
}
};
namespace std {
template<class T>
void swap<MyClass<T>>(MyClass<T>& lhs, MyClass<T>& rhs)noexcept { //错误,不该通过编译!!
lhs.swap(rhs);
}
}
- 与class版本类似,我们轻易的写出了swap的member版本和特化版本。但是,很不幸,C++不允许函数模板进行偏特化!!而更不幸的消息是,有的编译器会支持。
- 而当打算偏特化某个函数模板时(ps: 其实使用全特化函数模板时,我往往也会选择函数重载。),我们往往采用函数重载。
namespace std{
template <class T>
void swap(MyClass<T>& lhs, MyClass<T>& rhs){
lhs.swap(rhs);
}
}
- 注意,swap函数名后面没有<>,这是函数重载。
不要向std中增加东西
- 这种解决方法是很好的方法!(ps: 确实是很好的方法,在其他地方大有作为,只是在这里(std)不好。不要受下面的影响。)
- 但是对于std这个命名空间来说,就有问题。std的管理方式是:你可以特化某些已经存在于std的函数或者类,但是不允许你增加新的东西! 因为std里面的东西都是C++标准委员会制定的,如果程序员擅自添加东西到std里面,那么就会污染std命名空间,导致你的程序可能发生不明确行为。
- 那可如何是好?
- 解决方法也很简单,我们仍然需要一个member的swap和一个non-member版本的swap,non-member版本的swap只要不在std里面就可以。
namespace zzh { //新的命名空间
template <class T>
class MyClassNode {
//...
};
template <class T>
class MyClass {
//...
};
template<class T> //这个swap是一个模板函数,而非特化或者重载;
void swap<MyClass<T>>(MyClass<T>& lhs, MyClass<T>& rhs)noexcept {
lhs.swap(rhs);
}
}
- 这样写之,无论你在什么位置,只要妄图交换两个MyClass类型的变量,就会触发所谓的argument-dependent lookup法则,最终会查找到class的专属swap函数。
为你的class(非模板)补上特化的std::swap
- 理论上说,上面的处理已经很完美,无论是class还是template class,我们都不再需要为std特化swap函数。但是我们仍需要多一重考虑。
- 上述都是作为一个swap的编写者而言,如果你是一个swap调用者呢?
- 如果你在写一个函数,需要调用swap函数,那么你的希望是,优先调用类的专属版本的swap,如果没有,再调用std的default版本的swap。
void test(T& lhs, T& rhs){
using std::swap;
swap(lhs, rhs);
//其他操作省略。
}
- 这种写法会让编译器找到所有的swap并挑选出编译器最喜欢的(通常来说,也是你最喜欢的)。不管这个swap是在std内,还是在某个其他命名空间中。
- 编译器会去查找T类型是否有一个专属的swap函数,如果没有,我们的using std::swap会让std的swap暴露,给我们兜底。
- 注意:不要为swap前增加任何修饰,直接赤裸裸的调用swap。
void test(T& lhs, T& rhs){
std::swap(lhs, rhs); //这是错误的!
//其他操作省略。
}
- 这是一种错误的调用方法。因为这使得编译器强制调用std的swap,而不管你是否另外实现了类的专属swap。
- 所以,为了让这些迷途的代码回归正道,我们需要再为它们写一个std::swap的特化版本,这样std::swap就会调用到特化版本,最终调用到专属swap。
让member版的swap远离异常
- 我们的member版本的swap函数绝对不应该抛出异常。
- 因为swap函数为class和template class提供了强烈的异常安全性保证。而这一切基于swap是不会抛出异常的。
- swap成员版本往往提供的是更高效的swap,而且不抛出异常。特殊的swap交换的往往是内置类型,而内置类型的swap绝不会抛出异常。
C++11之"移动拷贝"版本的swap
-
将你卧室的电视机与客厅的电视机交换,应该如何做呢? 答:先将卧室的电视机移动到客厅,然后将客厅的电视机移动到卧室。但是C++不会这样做,在C++11之前,C++会先拷贝一个电视机。
-
C++11引入了移动语义和右值引用来提高效率,而swap函数也因此改写。
template <class T> void swap (T& a, T& b)
{
T c(std::move(a));
a=std::move(b);
b=std::move(c);
}
- 使用移动语义,swap交换就是我们生活中的交换:直接移动电视机即可,不需要进行“高端”的拷贝。
- 你可能已经意识到,即使我们不写类的memeber swap函数,std::swap也能高效的帮助我们交换两个对象。
本文主要来自:《EffectiveC++》