右值引用与移动语义是C11非常重要的语法我们在之前学的引用都是左值引用而我们今天要学习的是右值引用。右值引用与移动语义一、左值与右值二、左值引用与右值引用三、右值引用延长生命周期四、右值引用的参数匹配五、右值引用与移动语义使用场景1. 左值引用使用场景及作用2. 移动构造与移动赋值3. 右值引用与移动语义解决传值返回问题3.1 右值对象构造只有拷贝构造没有移动构造3.2 右值对象构造有拷贝构造也有移动构造3.3 右值对象赋值只有拷贝构造没有移动构造与移动赋值3.4 右值对象赋值有拷贝构造也有移动构造与移动赋值3.5 总结整理六、类型分类七、引用折叠八、完美转发一、左值与右值左值是一个表示数据的表达式(如变量名或解引用的指针)一般是有持久状态存储在内存中我们可以获取它的地址左值可以出现赋值符号的左边也可以出现在赋值符号右边。定义时const修饰符后的左值不能给他赋值但是可以取它的地址。右值也是一个表示数据的表达式要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边右值不能取地址。总的来说左值与右值最本质区别是能不能取地址。左值// 左值可以取地址 // 以下的p、b、c、*p、s、s[0]就是常⻅的左值 int* p new int(0); int b 1; const int c b; *p 10; string s(111111); s[0] x;右值// 右值不能取地址 double x 1.1, y 2.2; // 以下⼏个10、x y、fmin(x, y)、string(11111)都是常⻅的右值 10; x y; fmin(x, y); string(11111);二、左值引用与右值引用Type r1 x;就是左值引用左值引用就是给左值取别名Type rr1 y;就是右值引用同样的道理右值引用就是给右值取别名。左值引用不能直接引用右值但是const左值引用可以引用右值右值引用不能直接引用左值但是右值引用可以引用move(左值)。注意右值引用只是引用了右值对象但右值引用本身的属性是一个左值。语法层面看左值与右值都是取别名不开空间。从汇编层来看左值引用与右值引用都是用指针来实现没有区别。int x1; int r1x; int rr12;反汇编int r1 x; 00007FF7A6E918C5 lea rax,[x] 00007FF7A6E918C9 mov qword ptr [r1],rax int rr1 2; 00007FF7A6E918CD mov dword ptr [rbp64h],2 00007FF7A6E918D4 lea rax,[rbp64h] 00007FF7A6E918D8 mov qword ptr [rr1],rax可见两者底层都是用指针来实现的。// 左值可以取地址 // 以下的p、b、c、*p、s、s[0]就是常⻅的左值 int* p new int(0); int b 1; const int c b; *p 10; string s(111111); s[0] x; double x 1.1, y 2.2; // 左值引用给左值取别名 int r1 b; int* r2 p; int r3 *p; string r4 s; char r5 s[0]; // 右值引用给右值取别名 int rr1 10; double rr2 x y; double rr3 fmin(x, y); string rr4 string(11111); // 左值引用不能直接引用右值但是const左值引用可以引用右值 const int rx1 10; const double rx2 x y; const double rx3 fmin(x, y); const string rx4 string(11111); // 右值引用不能直接引用左值但是右值引用可以引用move(左值) int rrx1 move(b); int* rrx2 move(p); int rrx3 move(*p); string rrx4 move(s); string rrx5 (string)s; // b、r1、rr1都是变量表达式都是左值 // 这⾥要注意的是rr1的属性是左值所以不能再被右值引用绑定除⾮move一下 int r6 r1; // int rrx6 rr1; int rrx6 move(rr1);三、右值引用延长生命周期右值引用可以为临时对象延长生命周期虽然const左值引用也可以延长生命周期但是它不能被修改。std::string s1 Test; // std::string r1 s1; // 错误不能绑定到左值 const std::string r2 s1 s1; // OK到 const 的左值引用延⻓⽣存期 // r2 Test; // 错误不能通过到 const 的引用修改 std::string r3 s1 s1; // OK右值引用延⻓⽣存期 r3 Test; // OK能通过到⾮ const 的引用修改要注意的是这里值的延长生命周期是指上面右值对象的生命周期从一行延长到与r2,r3相同这不能推广到函数中创建的对象想通过右值返回值来延长生命周期是不可取的因为函数的栈帧是开在栈区函数结束立马销毁栈帧该右值引用所指 的右值也被销毁这个引用就成了“野引用”我们以下面的代码为例可以看到没有任何输出。四、右值引用的参数匹配C98中我们实现一个const左值引用作为参数的函数那么实参传递左值和右值都可以匹配。C11以后分别重载左值引用、const左值引用、右值引用作为形参的f函数那么实参是左值会匹配f(左值引用)实参是const左值会匹配f(const 左值引用)实参是右值会匹配f(右值引用)。void f(int x) { std::cout 左值引用重载 f( x )\n; } void f(const int x) { std::cout 到 const 的左值引用重载 f( x )\n; } void f(int x) { std::cout 右值引用重载 f( x )\n; } int main() { int i 1; const int ci 2; f(i); // 调用 f(int) f(ci); // 调用 f(const int) f(3); // 调用 f(int)如果没有 f(int) 重载则会调用 f(const int) f(std::move(i)); // 调用 f(int) // 右值引用变量在用于表达式时是左值 int x 1; f(x); // 调用 f(int x) f(std::move(x)); // 调用 f(int x) return 0; }五、右值引用与移动语义使用场景1. 左值引用使用场景及作用在我们以往使用左值引用返回值时主要目的就是为了减少拷贝同时还可以修改实参和返回对象的价值。左值引用已经解决了大多数问题但是对于下面的addStrings与generate的场景左值引用返回就无法很好地解决。那可以使用move(左值)将左值变成右值再返回吗上面已经演示过了显然是不行的。class Solution { public: // 传值返回需要拷⻉ string addStrings(string num1, string num2) { string str; int end1 num1.size() - 1, end2 num2.size() - 1; // 进位 int next 0; while (end1 0 || end2 0) { int val1 end1 0 ? num1[end1--] - 0 : 0; int val2 end2 0 ? num2[end2--] - 0 : 0; int ret val1 val2 next; next ret / 10; ret ret % 10; str (0 ret); } if (next 1) str 1; reverse(str.begin(), str.end()); return str; } // 这⾥的传值返回拷⻉代价就太⼤了 vectorvectorint generate(int numRows) { vectorvectorint vv(numRows); for (int i 0; i numRows; i) { vv[i].resize(i 1, 1); } for (int i 2; i numRows; i) { for (int j 1; j i; j) { vv[i][j] vv[i - 1][j] vv[i - 1][j - 1]; } } return vv; } };输出型参数的解决方法就是在函数中再引入一个输出值的引用的参数利用这个参数来返回。2. 移动构造与移动赋值部分string类代码class string { public: string(const char* str ) :_size(strlen(str)) ,_capacity(_size) { cout string(char* str)-构造 endl; _str new char[_capacity 1]; strcpy(_str, str); } // 拷贝构造 string(const string s) :_str(nullptr) { cout string(const string s) -- 拷贝构造 endl; reserve(s._capacity); for(auto ch : s) { push_back(ch); } } void swap(string ss) { ::swap(_str, ss._str); ::swap(_size, ss._size); ::swap(_capacity, ss._capacity); } // 移动构造 string(string s) { cout string(string s) -- 移动构造 endl; // 转移掠夺你的资源 swap(s); } string operator(const string s) { cout string operator(const string s) -- 拷贝赋值 endl; if (this ! s) { _str[0] \0; _size 0; reserve(s._capacity); for (auto ch : s) { push_back(ch); } } return *this; } // 移动赋值 string operator(string s) { cout string operator(string s) -- 移动赋值 endl; swap(s); return *this; } ~string() { //cout ~string() -- 析构 endl; delete[] _str; _str nullptr; } const char* c_str() const { return _str; } private: char* _str nullptr; size_t _size 0; size_t _capacity 0; };我们来看看它的移动构造是怎么实现的是怎么体现C11的优势的。我们知道当实参为右值时右值本身没有没有价值生命周期即将结束那么我们可以交换右值与原string的资源完成构造与赋值这也很好体现了右值引用为左值的作用可以被修改这种方式的效率明显比拷贝构造要高不少。我们来看看以下代码int main() { zzt::string s1(xxxxxx); //拷贝构造 zzt::string s2 s1; //构造移动构造优化为直接构造 zzt::string s3 zzt::string(114514); //移动构造 zzt::string s4 move(s1); cout *********************************** endl; cout s1.c_str() endl; return 0; }我们可以看到s4用move后的s1进行移动构造之后s1却变为空。我们在之前实现移动构造时我们默认的是右值对象不重要直接掠夺右值对象的资源因此s1的资源被s4掠夺变为空。因此我们不能随便move(左值)去移动构造对象。3. 右值引用与移动语义解决传值返回问题这里我们可以分为两个场景右值对象构造与赋值。// 场景1 int main() { zzt::string ret bit::addStrings(11111, 2222); cout ret.c_str() endl; return 0; } // 场景2 int main() { zzt::string ret; ret bit::addStrings(11111, 2222); cout ret.c_str() endl; return 0; }我们可以将它分成四种场景来探讨这也对应了没有移动语义和有移动语义的场景右值对象构造只有拷贝构造没有移动构造右值对象构造有拷贝构造也有移动构造右值对象赋值只有拷贝构造没有移动构造与移动赋值右值对象赋值有拷贝构造也有移动构造与移动赋值3.1 右值对象构造只有拷贝构造没有移动构造我们可以将代码复制到test.cpp文件中在linux环境下使用g test.cpp -fno-elide-constructors关闭编译器优化来运行。可以看到没有优化的情况下它是进行了两次拷贝构造。在VS2019 debug环境下编译器会将两次拷贝构造合为一次拷贝构造将str直接拷贝给ret。在VS2019 release及VS2022以上版本中编译器优化的更厉害它会直接优化成一次构造连拷贝构造都没有发生。这种情况下函数中的str就是ret的别名它们的地址是相同的。3.2 右值对象构造有拷贝构造也有移动构造无优化两次移动构造先移动构造给临时对象再移动构造给ret。VS2019 debug第一代优化两次移动构造变为一次移动构造与之前思路类似VS2019 release及VS2022以上第二代优化两次移动构造变为直接构造没有移动构造3.3 右值对象赋值只有拷贝构造没有移动构造与移动赋值无优化两次拷贝先将str拷贝构造给临时对象临时对象再拷贝赋值给ret。VS2019 debug第一代优化输出几乎没有变化只是对传值做了些许优化。VS2019 release及VS2022以上第二代优化构造拷贝赋值先构造临时对象str就是临时对象的引用再将临时对象拷贝赋值给ret。这个我们可以通过运行结果来验证str析构在拷贝赋值以后因此可以知道str就是ret的别名。3.4 右值对象赋值有拷贝构造也有移动构造与移动赋值无优化两次移动先将str移动构造给临时对象再将临时对象移动赋值给ret。VS2019 debug第一代优化输出几乎无变化。VS2019 release及VS2022以上第二代优化构造移动赋值先构造临时对象str就是临时对象的引用再将临时对象移动赋值给ret。与上面的情况类似。3.5 总结整理以下是对于上面情况的整理场景不优化第一次优化第二次优化① 右值对象构造只有拷贝构造C98无移动语义两次拷贝构造①str → 临时对象②临时对象 → ret两次拷贝优化为一次拷贝构造str → ret直接构造retstr与ret可看作同一个对象RVO/NRVO没有任何拷贝② 右值对象构造有拷贝构造 移动构造C11两次移动构造①str → 临时对象②临时对象 → ret两次移动优化为一次移动构造str → ret直接构造ret没有移动构造RVO/NRVO③ 右值对象赋值只有拷贝构造没有移动构造、移动赋值C98拷贝构造 拷贝赋值①str → 临时对象拷贝构造②临时对象 → ret拷贝赋值几乎没有变化仍是拷贝构造 拷贝赋值str成为返回值对象消除临时对象只剩一次拷贝赋值④ 右值对象赋值有拷贝构造 移动构造 移动赋值C11移动构造 移动赋值①str → 临时对象移动构造②临时对象 → ret移动赋值几乎没有变化仍是移动构造 移动赋值消除临时对象返回对象直接作为移动源只剩一次移动赋值六、类型分类C11对类型进行了划分右值(rvalue)被分为纯右值(prvalue)和将亡值(xvalue)。泛左值(glvalue)分为将亡值(xvalue)和左值(lvalue)。glvalue lvalue xvaluervalue prvalue xvaluexvalue 同时属于 glvalue 和 rvalue。类别有名字有身份能取地址能移动资源左值(lvalue)✔✔✔✘纯右值(prvalue)✘✘✘✔将亡值(xvalue)✔✔✔✔七、引用折叠C11的引用折叠主要应用于出现引用的引用的场景。C中不能直接定义引用的引用如int ri;这样会报错因此C11引入了引用折叠。我们可以通过typedef来实现引用的引用操作。那我们怎么判断引用的引用是左值引用还是右值引用呢这里有一个规定只有右值引用的右值引用才会折叠成右值引用其余都是左值引用如果传入const左值引用那么折叠后依旧是const左值引用我们来看几个案例定义两个函数f1和f2templateclass T void f1(T x) {} templateclass T void f2(T x) {}f1中当T为int时x为左值引用T为int时x也为左值引用。因此x始终为左值引用。f2中当T为int时x为左值引用T为int时x为右值引用。因此我们可以利用传参来控制x为左值引用还是右值引用。以上面f1,f2为函数int main() { typedef int lref; typedef int rref; int n 0; lref r1 n; // r1 的类型是 int lref r2 n; // r2 的类型是 int rref r3 n; // r3 的类型是 int rref r4 1; // r4 的类型是 int // 没有折叠-实例化为void f1(int x) f1int(n); f1int(0); // 报错 // 折叠-实例化为void f1(int x) f1int(n); f1int(0); // 报错 // 折叠-实例化为void f1(int x) f1int(n); f1int(0); // 报错 // 折叠-实例化为void f1(const int x) f1const int(n); f1const int(0); // 折叠-实例化为void f1(const int x) f1const int(n); f1const int(0); // 没有折叠-实例化为void f2(int x) f2int(n); // 报错 f2int(0); // 折叠-实例化为void f2(int x) f2int(n); f2int(0); // 报错 // 折叠-实例化为void f2(int x) f2int(n); // 报错 f2int(0); return 0; }我们再看一个万能引用的示例templateclass T void Function(T t) { int a 0; T x a; x; cout a endl; cout x endl endl; } int main() { // 10是右值推导出T为int模板实例化为void Function(int t) Function(10); // 右值 int a; // a是左值推导出T为int引用折叠模板实例化为void Function(int t) Function(a); // 左值 // std::move(a)是右值推导出T为int模板实例化为void Function(int t) Function(std::move(a)); // 右值 const int b 8; // a是左值推导出T为const int引用折叠模板实例化为void Function(const int t) // 所以Function内部会编译报错x不能 Function(b); // const 左值 // std::move(b)右值推导出T为const int模板实例化为void Function(const int t) // 所以Function内部会编译报错x不能 Function(std::move(b)); // const 右值 return 0; }八、完美转发在上面的Function(T t)中我们传左值t就是左值引用传右值t就是右值引用。但是我们之前讲过右值引用本身的属性是左值因此无论我们传什么进去t都是左值如果我们想要保持t原有的属性继续往下面Fun函数中传递此时就要用到完美转发。完美转发forward简介完美转发forward本质是⼀个函数模板他主要还是通过引用折叠的方式实现下⾯示例中传递给Function的实参是右值T被推导为int没有折叠forward内部t被强转为右值引用返回传递给Function的实参是左值T被推导为int引用折叠为左值引用forward内部t被强转为左值引用返回。void Fun(int x) { cout 左值引用 endl; } void Fun(const int x) { cout const 左值引用 endl; } void Fun(int x) { cout 右值引用 endl; } void Fun(const int x) { cout const 右值引用 endl; } templateclass T void Function(T t) { Fun(t); } int main() { //10是右值--T为int--t是int Function(10); //a是左值--T为int--t是int int a; Function(a); //move(a)是右值--T为int--t是int Function(move(a)); const int b 8; // b是左值--T为const int--t为const int Function(b); //move(b)是右值--T为const int--t为const int Function(move(b)); return 0; }结果左值引用 左值引用 左值引用 const 左值引用 const 左值引用不用完美转发forward结果全是左值引用或const左值引用。接下来用上forwardvoid Function(T t) { Fun(forwardT(t)); }结果右值引用 左值引用 右值引用 const 左值引用 const 右值引用恢复正常。