c++右值引用、移动语义、完美转发
1. 左值、右值、左值引用以及右值引用
- 左值:一般指的是在内存中有对应的存储单元的值,最常见的就是程序中创建的变量
- 右值:和左值相反,一般指的是没有对应存储单元的值(寄存器中的立即数,中间结果等),例如一个常量,或者表达式计算的临时变量
int x = 10 int y = 20 int z = x + y //x, y , z 是左值 //10 , 20,x + y 是右值,因为它们在完成赋值操作后即消失,没有占用任何资源
- 左值引用:C++中采用 &对变量进行引用,这种常规的引用就是左值引用
- 右值引用:右值引用最大的作用就是让一个左值达到类似右值的效果(下面程序举例),让变量之间的转移更符合“语义上的转移”,以减少转移之间多次拷贝的开销。右值引用符号是&&。
例如,对于以下程序,我们要将字符串放到vector中,且我们后续的代码中不再用到x:
std::vector<std::string> vec; std::string x = "abcd"; vec.push_back(x); std::cout<<"x: "<<x<<"\n"; std::cout<<"vector: "<< vec[0]<<"\n"; //-------------output------------------ // x: abcd // vector: abcd
该程序在真正执行的过程中,实际上是复制了一份字符串x,将其放在vector中,这其中多了一个拷贝的开销和内存上的开销。但如果x以及没有作用了,我们希望做到的是 真正的转移,即x指向的字符串移动到vector中,不需要额外的内存开销和拷贝开销。因此我们希望让变量 x传入到push_back 表现的像一个右值 ,这个时候就体现右值引用的作用,只需要将x
的右值引用传入就可以。
改进成如下代码:
std::vector<std::string> vec; std::string x = "abcd"; vec.push_back(std::move(x)); <--------------- 使用了std::move,任何的左值/右值通过std::move都转化为右值引用 std::cout<<"x: "<<x<<"\n"; std::cout<<"vector: "<< vec[0]<<"\n"; //-------------output------------------ // x: // vector: abcd
可以看到,完成`push_back`后x
是空的。
2. 移动语义
移动语义是通过移动构造和移动赋值避免无意义的拷贝操作。
2.1 使用std::move实现移动构造
定义:采用右值引用作为参数的构造函数又称作移动构造函数。此时不需要额外的拷贝操作,也不需要新分配内存。
使用场景:对于一个值(比如数组、字符串、对象等)如果在执行某个操作后不再使用,那么这个值就叫做将亡值(Expiring Value),因此对于本次操作我们就没必要对该值进行额外的拷贝操作,而是希望直接转移,尽可能减少额外的拷贝开销,操作后该值也不再占用额外的资源。
使用函数:std::move,任何的左值/右值通过std::move都转化为右值引用
看如下例子,
#include <iostream> #include <vector> #include <string> class A { public: A(){} A(size_t size): size(size), array((int*) malloc(size)) { std::cout << "create Array,memory at: " << array << std::endl; } ~A() { free(array); } A(A &&a) : array(a.array), size(a.size) { a.array = nullptr; std::cout << "Array moved, memory at: " << array << std::endl; } A(A &a) : size(a.size) { array = (int*) malloc(a.size); for(int i = 0;i < a.size;i++) array[i] = a.array[i]; std::cout << "Array copied, memory at: " << array << std::endl; } size_t size; int *array; }; int main() { std::vector<A> vec; A a = A(10); vec.push_back(a); return 0; } //----------------output-------------------- // create Array,memory at: 0x600002a28030 // A a = A(10); 调用了 构造函数A(size_t size){} // Array copied, memory at: 0x600002a28050 //vec push的时候拷贝一份,调用构造函数A(A &a){}
从输出可以看到,每次进行push_back的时候,会重新创建一个对象,调用了左值引用A(A &a) : size(a.size)
对应的构造函数,将对象中的数组重新深拷贝一份。
如果该用右值引用进行优化,如下
int main () { std::vector<A> vec; A a = A(10); vec.push_back(std::move(a)); return 0; } //----------------output-------------------- // create Array,memory at: 0x600003a84030 // Array moved, memory at: 0x600003a84030
可以看到,这个时候虽然也重新创建了一个对象,但是调用的是这个构造函数A(A &&a) : array(a.array), size(a.size)
(这种采用右值引用作为参数的构造函数又称作移动构造函数),此时不需要额外的拷贝操作,也不需要新分配内存。
3. 完美转发
使用函数:std::forward,如果传递的是左值转发的就是左值引用,传递的是右值转发的就是右值引用。
3.1 引用折叠
在具体介绍std::forward之前,需要先了解C++的引用折叠规则,对于一个值引用的引用最终都会被折叠成左值引用或者右值引用。
- T& & -> T& (对左值引用的左值引用是左值引用)
- T& && -> T& (对左值引用的右值引用是左值引用)
- T&& & ->T& (对右值引用的左值引用是左值引用)
- T&& && ->T&& (对右值引用的右值引用是右值引用)
总结一句话,只有对于右值引用的右值引用折叠完还是右值引用,其他都会被折叠成左值引用。
3.2 使用std::forward实现完美转发
std::forward的作用就是完美转发,确保转发过程中引用的类型不发生任何改变,左值引用转发后一定还是左值引用,右值引用转发后一定还是右值引用!
下面是一个使用 std::forward 的例子:
#include <iostream> #include <utility> void func(int& x) { std::cout << "lvalue reference: " << x << std::endl; } void func(int&& x) { std::cout << "rvalue reference: " << x << std::endl; } template<typename T> void wrapper(T&& arg) { func(std::forward<T>(arg)); } int main() { int x = 42; wrapper(x); // lvalue reference: 42 wrapper(1); // rvalue reference: 1 return 0; }
在上面的例子中,我们定义了两个函数 func,一个接受左值引用,另一个接受右值引用。然后我们定义了一个模板函数 wrapper,在 wrapper 函数中,我们使用 std::forward 函数将参数 arg 转发给 func 函数。通过使用 std::forward,我们可以确保 func 函数接收到的参数的左右值特性与原始参数保持一致。
- 当向wrapper里面传入x的时候,wrapper推导认为 T是一个左值引用int &,通过引用折叠原则(看万能引用文章)int && + & = int &,相当于wrapper(int& arg),同时我们知道了T推导为int&,那么在向func传递的时候,就是func(std::forward<int&> (arg)) ,那么func会以左值引用的形式 func(int& x) 调用arg。
- 当向wrapper里面传入1的时候,wrapper推导认为T是一个右值引用int&& ,通过引用折叠原则,int && + && =int&& ,相当于wrapper(int&& arg),同时我们知道了T推导为int&&,那么在向func传递的时候,就是func(std::forward<int&&>(arg)),那么func会以左值引用的形式func(int&& x)调用arg。
另一个例子:
class Test{}; void B(Test& a) {cout << "B&" << endl;} void B(Test&& a) {cout << "B&&" << endl;} template<typename T> void A(T &&a) { B(std::forward<T>(a)); } int main() { Test a; A(std::move(a)); A(a); return 0; } ////// //输出结果 B&& B&
参考链接:https://zhuanlan.zhihu.com/p/469607144
https://www.jb51.net/article/278300.htm