移动语义、完美转发
C++11新增右值引用,谈到右值引用时可以扩展到一些相关概念:
- 左值
- 右值
- 纯右值
- 将亡值
- 左值引用
- 右值引用
- 移动语义
- 完美转发
- 返回值优化
一、左值、右值
概念1
左值:可以放到括号左边的东西叫左值
右值:不可以放到括号左边的东西就叫右值
概念2
左值:可以取地址并且有名字的东西就是左值
右值:不能取地址的没有名字的东西就是右值
举例:
1 int a = b + c;
a是左值,有变量名,可以取地址,也可以放到等号左边。表达式b+c的返回值是右值,没有名字且不能取地址,&(a+b)不能通过编译,而且也不能放到等号左边。
1 int a= 4; //a是左值,4 作为普通字面量是右值
左值一般有:
- 函数名和变量名
- 返回左值引用的函数调用
- 前置自增减表达式++i、--i
- 由赋值表达式或赋值运算符连接的表达式(a=b,a+=b等)
- 解引用表达式*p
- 字符串字面值"abcd"
二、纯右值、将亡值
纯右值和将亡值都属于右值。
纯右值
运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
举例:
- 除字符串字面值外的字面值
- 返回非引用类型的函数调用
- 后置自增减表达式i++、i--
- 算术表达式(a+b,a*b, a&&b,a==b等)
- 取地址表达式等(&a)
将亡值
将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值。将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间的方式获取的值,在确保其它变量不再被使用或即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。
举例:
1 class A { 2 xxx; 3 }; 4 A a; 5 auto c = std::move(a); // c是将亡值 6 auto d = static_cast<A&&>(a); // d是将亡值
三、左值引用、右值引用
根据名字大概就可以猜到意思,左值引用就是对左值进行引用的类型,右值引用就是对右值进行引用的类型,它们都是引用,都是对象的一个别名,并不拥有所绑定对象的堆内存,所以都必须立即初始化。
1 type &name = exp; // 左值引用 2 type &&name = exp; // 右值引用
左值引用
1 int a = 5; 2 int &b = a; // b是左值引用 3 b = 4; 4 int &c = 10; // error,10无法取地址,无法进行引用 5 const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址
可以得出结论:对左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式,但这样就只能通过引用来读取输出,不能修改数值,因为是常量引用。
右值引用
如果使用右值引用,那表达式等号右边的值需要是右值,可以使用std::move函数强制把左值转换为右值。
1 int a = 4; 2 int &&b = a; // error, a是左值 3 int &&c = std::move(a); // ok
四、移动语义
深拷贝、浅拷贝
1 class A { 2 public: 3 A(int size) : size_(size) { 4 data_ = new int[size]; 5 } 6 A(){} 7 A(const A& a) { 8 size_ = a.size_; 9 data_ = a.data_; 10 cout << "copy " << endl; 11 } 12 ~A() { 13 delete[] data_; 14 } 15 int *data_; 16 int size_; 17 }; 18 int main() { 19 A a(10); 20 A b = a; 21 cout << "b " << b.data_ << endl; 22 cout << "a " << a.data_ << endl; 23 return 0; 24 }
输出:
1 copy 2 b 0x100750b60 3 a 0x100750b60 4 CPP(893,0x1000ffd40) malloc: *** error for object 0x100750b60: pointer being freed was not allocated 5 CPP(893,0x1000ffd40) malloc: *** set a breakpoint in malloc_error_break to debug
上面代码中,两个输出的是相同的地址,a和b的data_指针指向来同一块内存,这就是浅拷贝,只是数据的简单赋值,那再析构时data_内存会被释放两次,导致程序出问题。如何消除这种隐患呢?可以使用如下深拷贝:
1 class A{ 2 public: 3 A(int size):size_(size){ 4 data_ = new int[size]; 5 } 6 A(){} 7 A(const A& a){ 8 size_ = a.size_; 9 data_ = new int[size_]; 10 memcpy(data_, a.data_,size_); 11 std::cout<<"copy"<<std::endl; 12 } 13 ~A(){ 14 delete [] data_; 15 } 16 int *data_; 17 int size_; 18 }; 19 int main(int argc, const char * argv[]) { 20 // insert code here... 21 A a(10); 22 A b = a; 23 std::cout<<"b "<<b.data_<<std::endl; 24 std::cout<<"a "<<a.data_<<std::endl; 25 return 0; 26 }
输出:
1 copy 2 b 0x1051c65d0 3 a 0x1051c65a0
深拷贝就是在拷贝对象时,如果被拷贝的内部还有指针指向其它资源,自己需要重新开辟一块新内存资源,而不是简单的赋值。
移动语义
可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存来复制对方资源,而对于移动语义,类似于转让或者资源窃取的意思,将那块资源转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢?答案是通过移动构造函数。
1 class A{ 2 public: 3 A(int size):size_(size){ 4 data_ = new int[size]; 5 } 6 A(){} 7 A(const A& a){ 8 size_ = a.size_; 9 data_ = new int[size_]; 10 memcpy(data_, a.data_,size_); 11 std::cout<<"copy"<<std::endl; 12 } 13 A(A&& a){ 14 this->data_ = a.data_; 15 a.data_=nullptr; 16 std::cout<<"move "<<endl; 17 } 18 ~A(){ 19 if(data_ != nullptr) 20 { 21 delete [] data_; 22 } 23 } 24 int *data_; 25 int size_; 26 }; 27 int main(int argc, const char * argv[]) { 28 // insert code here... 29 A a(10); 30 A b = a; 31 std::cout<<"a "<<a.data_<<std::endl; 32 A c=std::move(a);//调用移动构造函数 33 std::cout<<"b "<<b.data_<<std::endl; 34 std::cout<<"c "<<c.data_<<std::endl; 35 std::cout<<"a "<<a.data_<<std::endl; 36 return 0; 37 }
输出:
1 copy 2 a 0x1007c2630 3 move 4 b 0x1007c2660 5 c 0x1007c2630 6 a 0x0
如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的STL都实现了移动语义,方面我们使用。例如:
1 std::vector<string> vecs; 2 ... 3 std::vector<string> vecm = std::move(vecs); // 免去很多拷贝
注意:
移动语义仅针对哪些实现了移动构造函数的类对象,对于那种基本类型int、float等没有任何优惠作用吗还是会拷贝,因为他们没有实现对应用等移动构造函数。
完美转发
完美转发指可以写一个接受任意实参的函数模版,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实惨是右值那目标函数实参是右值那目标函数的实参也是右值。如何实现完美转发呢,答案是使用std::forward()。
1 void PrintV(int &t){ 2 std::cout<<"lvalue"<<std::endl; 3 } 4 void PrintV(int &&t){ 5 std::cout<<"rvalue"<<std::endl; 6 } 7 template<typename T> 8 void Test(T &&t){ 9 PrintV(t); 10 PrintV(std::forward<T>(t)); 11 PrintV(std::move(t)); 12 } 13 int main(int argc, const char * argv[]) { 14 // insert code here... 15 Test(1); // lvalue rvalue rvalue 16 int a = 1; 17 Test(a); // lvalue lvalue rvalue 18 Test(std::forward<int>(a)); // lvalue rvalue rvalue 19 Test(std::forward<int&>(a)); // lvalue lvalue rvalue 20 Test(std::forward<int&&>(a)); // lvalue rvalue rvalue 21 return 0; 22 }
输出:
1 lvalue 2 rvalue 3 rvalue 4 lvalue 5 lvalue 6 rvalue 7 lvalue 8 rvalue 9 rvalue 10 lvalue 11 lvalue 12 rvalue 13 lvalue 14 rvalue 15 rvalue
分析
- Test(1):1是右值,模版中T &&t这种为万能引用,右值1传到了Test函数中变成了右值引用,但是调用PrintV的时候,t变成了左值,因为它变成了一个拥有名字的变量,所以打印lvalue,而PrintV(std::forward<T>(t))时,会进行完美转发,按照原来的类型转发,所以打印rvalue,PrintV(std::move(t))豪无疑问会打印rvalue。
- Test(a):a是左值,模版中T &&t这种为万能引用,左值a传到Test函数中变成了左值引用,所以打印lvalue,lvalue,rvalue
- Test(std::forward<T>(a)):转发为左值还是右值,依赖于T,T是左值那就转发为左值,T是右值那就转发为右值。
5、返回值优化
返回值优化(RVO)是一种C++编译优化技术,当函数需要返回一个对象实例时,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。
那什么时候编译器会进行返回值优化呢?
- return的值类型与函数的返回值类型相同
- return的是一个举报对象
示例一
1 std::vector<int> return_vector(void) { 2 std::vector<int> tmp {1,2,3,4,5}; 3 return tmp; 4 } 5 std::vector<int> &&rval_ref = return_vector();
不会触发RVO,拷贝构造了一个临时的对象,临时对象的生命周期和rval_ref绑定,等价于下面这段代码:
1 const std::vector<int>& rval_ref = return_vector();
示例二
1 std::vector<int>&& return_vector(void) { 2 std::vector<int> tmp {1,2,3,4,5}; 3 return std::move(tmp); 4 } 5 6 std::vector<int> &&rval_ref = return_vector();
这段代码会运行时错误,因为rval_ref引用了被析构的tmp。
示例三
1 std::vector<int> return_vector(void) { 2 std::vector<int> tmp {1,2,3,4,5}; 3 return std::move(tmp); 4 } 5 6 std::vector<int> &&rval_ref = return_vector();
和示例一类是,std::move一个临时对象没有必要,也会忽略掉返回值。
最好的代码
1 std::vector<int> return_vector(void) { 2 std::vector<int> tmp {1,2,3,4,5}; 3 return tmp; 4 } 5 6 std::vector<int> rval_ref = return_vector();
这段代码会触发RVO,不会拷贝也不会移动,不生成临时对象。