右值引用与移动语义

1725 字
9 分钟
右值引用与移动语义

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)的场景下,出了函数作用域局部对象就被销毁了,因此不能使用左值引用返回,只能传值返回。

传统的解决方案:

  1. 使用输出型参数(通过传引用参数把结果带出来)—— 这会牺牲代码可读性。

  2. 依赖编译器自身的非标准优化。

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 关闭优化来观察原本的执行过程)。

以返回局部对象为例,经历的演变如下:

  • 不优化:局部对象 →\rightarrow 拷贝/移动构造出临时对象 →\rightarrow 临时对象拷贝/移动构造给接收对象。(共产生两次构造)

  • 一代优化:直接将 局部对象 →\rightarrow 拷贝/移动构造给接收对象。(省去了临时对象,合二为一)

  • **二代优化 **:直接在接收对象的空间上构造局部对象,合三为一,变为直接构造。此时局部对象本质上就是外层接收对象的引用,两者地址完全相同。

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

  • 拷贝赋值


3. C++11 类型细分#

C++11 进一步细化了值类别,将表达式分为三大类、两大派:

  • 泛左值 (glvalue - generalized lvalue):包含 左值 和 将亡值。

  • 右值 (rvalue):包含 纯右值 和 将亡值。

细分定义:

  1. 左值 (lvalue):有名字,且不能被 move。

  2. 纯右值 (prvalue - pure rvalue):没有名字,且可以被移动(即 C++98 中的右值)。如:42、true、nullptr、传值返回的函数调用、a+b 等不具名临时对象。

  3. 将亡值 (xvalue - expiring value):有名字,且可以被 move。通常是指返回右值引用的函数调用表达式,或转换为右值引用的转换函数调用(如 std::move(x)、static_cast<X&&>(x))。


4. 万能引用与引用折叠#

4.1 引用折叠#

C++ 语法不允许直接写出引用的引用(如 int& && 会报错),但在模板推导或 typedef 时会自动触发引用折叠规则:

  • 右值引用的右值引用 折叠成 右值引用(&& + && →\rightarrow &&)。

  • 所有其他组合 均折叠成 左值引用(&+&、&+&&、&&+& →\rightarrow &)。

// 由于引⽤折叠限定,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);

    配合完美转发,可以确保右值元素在整个插入链路中保持右值特性,从而精准触发移动语义,大幅提升性能。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
HandlerOne
おかえり
公告
本站已整理 138 篇技术笔记,并按 8 条主线排成学习路线(141 站);站内搜索与桌宠对话均已上线。
分类
标签
最新动态
站点统计
文章
138
分类
12
标签
42
总字数
597,044
站点信息
构建平台
腾讯云 · Nginx
博客版本
v6.16.8
文章目录