swap函数的高效实现:右值引用和move
2016-03-03 21:50
375 查看
原创内容:cxsmarkchan
转载请注明出处
右值引用是C++11的重要特性之一,合理采用右值引用,可以避免不必要的复制操作,提高程序运行的效率。本文以swap函数的实现为例,说明右值引用的相关应用。
如果T是简单类型,则上述转换没有问题。但如果T是含有指针的复合数据类型,则上述转换中会调用一次复制构造函数,两次赋值运算符重载。以一个向量类型为例:
不难看出,复制构造函数和赋值运算符重载函数的复杂度均为O(_size),因此swap函数的复杂度亦为O(_size)。因此,这并不是一个高效的交换方法。
其实,执行
一种变通的解决方案是如下函数:
该函数在交换vector变量时,复杂度为O(1)。不足之处是,
右值引用可以很好地解决这一问题。
其中的
右值引用也可以作为参数传入函数中:
在以上例子中,可以看出左值引用和右值引用的应用场景。对于
移动构造函数和移动赋值运算符是比较典型的应用,如2.3节。
与第1节的复制构造函数和赋值运算符相比,移动构造函数和移动赋值运算符实现了右值中一些动态对象的“废物利用“,无需进行复制操作,从而可以极大地提高效率。
此时,swap中三次调用的均为move类函数,而这些函数的时间复杂度均为O(1),因此swap的整体时间复杂度为O(1)。
转载请注明出处
右值引用是C++11的重要特性之一,合理采用右值引用,可以避免不必要的复制操作,提高程序运行的效率。本文以swap函数的实现为例,说明右值引用的相关应用。
1 swap函数的传统实现方法及其问题
一个典型的swap函数如下:template<class T> void swap(T& a, T& b){ T tmp = a; //调用复制构造函数 a = b; //调用operatpr= b = tmp; //调用operatpr= }
如果T是简单类型,则上述转换没有问题。但如果T是含有指针的复合数据类型,则上述转换中会调用一次复制构造函数,两次赋值运算符重载。以一个向量类型为例:
class vector{ private: int *_arr; int _size; public: //仅考虑复制构造函数和赋值运算符重载 vector(const vector& _right){ _size = _right._size; _arr = new int[_size]; for(int i = 0; i < _size; i++){ _arr[i] = _right._arr[i]; //该步骤需要重复_size次 } } vector& operator=(const vector& _right){ if(this != &_right){ if(_arr != NULL) delete[] _arr; _size = _right._size; _arr = new int[_size]; for(int i = 0; i < _size; i++){ _arr[i] = _right._arr[i]; //该步骤需要重复_size次 } } } //析构函数 ~vector(){ if(_arr != NULL) delete[] _arr; } };
不难看出,复制构造函数和赋值运算符重载函数的复杂度均为O(_size),因此swap函数的复杂度亦为O(_size)。因此,这并不是一个高效的交换方法。
其实,执行
T tmp = a时,本可以将
tmp._arr直接指向
a._arr,而不用执行一遍复制操作。遗憾的是,程序在执行
T tmp = a时,会假设
a和
tmp是两个不同的变量,且将来都会被调用,因此只能将
a复制一份给
tmp。
一种变通的解决方案是如下函数:
void swap(vector& var1, vector& var2){ int* _tmp_arr = var1._arr; var1._arr = var2._arr; var2._arr = _tmp_arr; int _tmp_size = var1._size; var1._size = var2._size; var2._size = _tmp_size; }
该函数在交换vector变量时,复杂度为O(1)。不足之处是,
_arr和
_size是私有变量,需要通过友元函数来实现;同时对于每个不同的类型,都需要重写一遍swap函数。
右值引用可以很好地解决这一问题。
2 右值引用和move语义
2.1 右值
表达式的值可以分为左值和右值。左值可以出现在赋值表达式的左端或右端,右值则只能出现在赋值表达式的右端。例如:string a; a = "1234";
其中的
a即为左值,而
"1234"则为右值。简单地说,左值是一定意义上的持久变量,在表达式执行完后仍可以存在,右值则是临时变量,在表达式执行完毕后,就会被销毁。此外,一种比较简单的区分方式是:左值可以执行取地址操作,右值则不可以。从这个角度来说,有定义的变量均为左值,而函数返回值则为右值。
2.2 右值引用
所谓右值引用,则是指向右值的引用,可以用&&来定义:
int &&a = 1;
右值引用也可以作为参数传入函数中:
// 假设Class1为有定义的类型 Class1 f1(){...} void f2(const Class1&){...} void f2(Class1&&){...} int main(){ Class1 c1; f2(c1); //调用f2(Class1&) f2(f1()); //调用f2(Class1&&) }
在以上例子中,可以看出左值引用和右值引用的应用场景。对于
f2(c1),传入的是左值引用,该函数中必须假设参数
c1在将来还有别的用处,因此不能轻易对
c1的值进行修改,如果
f2中要用到
c1中动态开辟的对象,就需要复制一份;而
f2(f1())中,传入的是右值引用,函数中已知传入的参数在本次调用后就会被销毁,可以直接将传入参数中的某些动态开辟的对象“据为己有”,无需复制。
移动构造函数和移动赋值运算符是比较典型的应用,如2.3节。
2.3 移动构造函数和移动赋值运算符
移动构造函数和移动赋值运算符如下://以下函数均在vector类中 //移动(move)构造函数 vector(vector&& _right){ _size = _right._size; _arr = _right._arr; //由于知道_right引用的是临时变量,不会被继续使用,因此可以把_right._arr直接拿过来(即移动) _right._arr = NULL; //这个语句是表示_right对_arr已经失去所有权,防止_right在析构时销毁_arr。 } //移动赋值运算符 vector& operator=(vector&& _right){ if(this != &_right){ if(_arr != NULL) delete[] _arr; _size = _right._size; _arr = _right._arr; //同样地,知道_right是右值引用,就可以直接把_right._arr拿过来。 _right._arr = NULL; } }
与第1节的复制构造函数和赋值运算符相比,移动构造函数和移动赋值运算符实现了右值中一些动态对象的“废物利用“,无需进行复制操作,从而可以极大地提高效率。
2.4 std::move函数
std::move函数可以将左值转换为右值,该做法的意义在于:如果已知某个左值将来不会再用到,就可以用该函数将其转换为右值,传入含右值引用的函数中,进行”废物利用“。例如:
Class1 c1; f2(c1); //调用f2(Class1&)函数 f2(std::move(c1)); //调用f2(Class1&&)函数,但程序员需要保证之后不会再用到c1的数据。
3 swap函数的高效实现
综合上述内容,swap函数的一个高效实现方法为:template<class T> void swap(T& var1, T& var2){ T tmp = std::move(var1); //调用移动构造函数 var1 = std::move(var2); //调用operator=(T&&) var2 = std::move(tmp); //调用operator=(T&&) }
此时,swap中三次调用的均为move类函数,而这些函数的时间复杂度均为O(1),因此swap的整体时间复杂度为O(1)。
4 小结
右值引用表示该引用对象今后将不再使用,在对象被析构之前,可以回收利用其中的一些有效信息。合理地运用右值引用,可以提高程序的运行效率。建议在含有动态对象的类中,都要写移动构造函数和移动赋值运算符重载函数。同时,如果确认某些对象不再使用,也可以使用move函数传入其右值引用。相关文章推荐
- 二叉树的先序,中序,后序遍历实现
- 对肺结节几何矩的特征提取
- 【C/C++学院】0907-象棋五子棋代码分析/寻找算法以及排序算法
- 机器学习之路——多重共线性
- Scala中class和object的区别
- c#接口与抽象类的区别
- 第一次作业 邱鹏 2013551628
- bzoj 1492 [NOI2007]货币兑换Cash(斜率dp+cdq分治)
- linux中将可执行程序做成一个服务
- TCP/IP基础(四)
- Android和Js交互
- [iOS]利用通知实现监听系统键盘
- Windows下,给Nim程序添加图标
- 15 几个Calender相关的方法
- 单片机第13课:串口通信---向计算机发送数据
- 1047. Student List for Course (25)
- class "org.apache.commons.collections.FastHashMap"'s signer information does not match signer inform
- _CRT_SECURE_NO_WARNINGS
- [iOS]为什么不要在init初始化方法里调用self.view
- Problem A: 写一个函数,使给定的一个二维数组(3×3)转置,即行列互换