尝试写出一个不带有异常的swap

本文探讨了C++中优化swap函数的方法,包括在类内实现member swap并特化std::swap,以避免不必要的数据拷贝,提高效率。同时强调了member swap应确保不抛出异常,以保证异常安全。C++11引入的移动语义进一步提升了swap的性能,无需额外的成员函数。最后,讨论了如何在避免污染std命名空间的同时,确保调用到最佳的swap实现。
摘要由CSDN通过智能技术生成

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++》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值