右值引用与移动语义
1. 左值与右值
传统C++98中只有引用的概念(即现在的左值引用),C++11 新增了右值引用,两者都是给对象取别名。
-
左值引用:
Type& x -
右值引用:
Type&& y
1.1 概念对比
-
左值 (lvalue - locator value):表示数据的表达式(如变量名、解引用的指针)。它存储在内存中,有持久状态,可以取地址。它可以出现在赋值符号的左边或右边。如果是
const修饰的左值,虽不能赋值,但依然可以取地址。 -
右值 (rvalue - read value):表示数据的表达式(如字面量常量、表达式求值过程中创建的临时对象)。它提供数据值,但不能取地址,只能出现在赋值符号的右边。
1.2 引用绑定规则
- 左值引用:只能绑定左值(给左值取别名)。例外:
const左值引用可以绑定右值。
int& a = b; //绑定左值 const int& c = 10; //const左值引用绑定右值- 右值引用:只能绑定右值(给右值取别名)。例外:右值引用可以绑定经过
std::move()转换的左值。
int&& a = 10; // 绑定右值 int&& b = move(c); // 右值引⽤可以引⽤move(左值)右值引用的属性
一个右值被右值引用变量绑定后,该变量表达式的属性就变成了左值。
(从汇编底层看,左值引用和右值引用的底层实现都是指针,并没有区别。不要把底层实现和上层语法混为一谈。)
2. 移动语义与 std::move
2.1 为什么需要移动语义?(左值引用的局限性)
左值引用在传参和做返回值时大大减少了拷贝,提高了效率。但在返回局部对象(如 addStrings 和 generate 函数内部创建的 string 或 vector)的场景下,出了函数作用域局部对象就被销毁了,因此不能使用左值引用返回,只能传值返回。
传统的解决方案:
-
使用输出型参数(通过传引用参数把结果带出来)—— 这会牺牲代码可读性。
-
依赖编译器自身的非标准优化。
C++11 的终极解决方案:右值引用和移动语义。
2.2 移动构造与移动赋值
对于像 string、vector 这种涉及深拷贝的类,实现移动构造和移动赋值具有极大价值(对于浅拷贝类如 Date 则没必要)。
-
移动构造:第一个参数是该类类型的右值引用(如果有其他参数必须有缺省值)。它不分配新空间,而是直接**“窃取”**右值对象的资源。
-
移动赋值:重载赋值运算符,第一个参数同样是该类的右值引用。
// 移动构造string(string &&s){ swap(s);}
// 移动赋值string &operator=(string &&s){ swap(s); return *this;}
string s = move(s1);2.3 编译器对返回值的优化 (RVO)
在函数返回对象时,编译器会进行不同程度的优化(可通过在 Linux 下使用 g++ test.cpp -fno-elide-constructors 关闭优化来观察原本的执行过程)。
以返回局部对象为例,经历的演变如下:
-
不优化:局部对象 拷贝/移动构造出临时对象 临时对象拷贝/移动构造给接收对象。(共产生两次构造)
-
一代优化:直接将 局部对象 拷贝/移动构造给接收对象。(省去了临时对象,合二为一)
-
**二代优化 **:直接在接收对象的空间上构造局部对象,合三为一,变为直接构造。此时局部对象本质上就是外层接收对象的引用,两者地址完全相同。

-
str明明是左值为什么调用移动构造?:C++11 标准规定:在返回非引用类型的局部变量时,编译器会优先将其视为右值(自动将其当作
std::move(str)来处理),从而去匹配移动构造函数,而不是拷贝构造函数。 -
拷贝赋值

3. C++11 类型细分
C++11 进一步细化了值类别,将表达式分为三大类、两大派:
-
泛左值 (glvalue - generalized lvalue):包含 左值 和 将亡值。
-
右值 (rvalue):包含 纯右值 和 将亡值。
细分定义:
-
左值 (lvalue):有名字,且不能被 move。
-
纯右值 (prvalue - pure rvalue):没有名字,且可以被移动(即 C++98 中的右值)。如:
42、true、nullptr、传值返回的函数调用、a+b等不具名临时对象。 -
将亡值 (xvalue - expiring value):有名字,且可以被 move。通常是指返回右值引用的函数调用表达式,或转换为右值引用的转换函数调用(如
std::move(x)、static_cast<X&&>(x))。
4. 万能引用与引用折叠
4.1 引用折叠
C++ 语法不允许直接写出引用的引用(如 int& && 会报错),但在模板推导或 typedef 时会自动触发引用折叠规则:
-
右值引用的右值引用 折叠成 右值引用(
&&+&&&&)。 -
所有其他组合 均折叠成 左值引用(
&+&、&+&&、&&+&&)。
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤template <class T>void f1(T &x){}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤//因此也被称为万能引用template <class T>void f2(T &&x){}4.2 万能引用 (Universal Reference)
在函数模板中,形如 T&& x 的参数被称为万能引用:
- 当传入左值时,
T被推导为int&,折叠后形参为左值引用。
// 折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0); // 报错- 当传入右值时,
T被推导为int,折叠后形参为右值引用。
// 折叠->实例化为void f2(int&& x)f2<int&&>(0);f2<int&&>(n); // 报错5. 完美转发 (Perfect Forwarding)
5.1 需要完美转发的原因
如前所述,右值引用变量本身的属性是左值。如果我们在模板函数内部,将接收到的右值引用参数继续传递给内层函数,它会以左值的身份去匹配下一层函数,从而导致右值属性的丢失。
5.2 std::forward
为了在传参过程中保持对象原有的值属性(左值或右值),必须使用完美转发 std::forward<T>(arg)。
-
本质:
std::forward也是一个函数模板,内部巧妙利用了引用折叠规则。-
实参是右值:没有折叠,内部强制转换为右值引用返回。
-
实参是左值:发生引用折叠,内部强制转换为左值引用返回。
-
应用实例:
C++11 之后,标准库容器(如 std::list)的插入接口全面升级:
-
C++98:
void push_back (const value_type& val); -
C++11 新增:
void push_back (value_type&& val);配合完美转发,可以确保右值元素在整个插入链路中保持右值特性,从而精准触发移动语义,大幅提升性能。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

