C++左值与右值

说明

  • 这一部分内容只是帮助理解 C++(11) 中左值与右值的概念。

  • 在编程实践中,因为编译器优化的存在,特别是其中的返回值优化(Return Value Optimization, RVO)使你不需要额外关注左值与右值的区别,像 C++(03) 一样编程即可。

    C++11 rvalues and move semantics confusion (return statement) - Stack Overflow

  • 除非你在进行库的开发,特别是涉及模板元编程等内容时,需要实现移动构造函数(move constructor),或者完美转发

Index

小结

左值引用类型 与 右值引用类型

这里的变量 t1~t9 都是左值,因为它们都有名字

当发生自动类型推断时,T&& 也能绑定左值

  • 此时,T&& 就不再是右值引用类型,而是未定引用类型

如何快速判断左值与右值

  • 能被 & 取地址的就是左值

    • 多数常数、字符等字面量都是右值,但字符串是左值

    • 虽然字符串字面量是左值;但它是 const 左值(只读对象),所以也不能对它赋值

      为什么字符串字面量是对象?——节省内存,同一份字符串字面量引用的是同一块内存

  • 所有的具名变量或对象都是左值,而匿名变量/临时变量则是右值

    • 匿名变量/临时变量的特点是表达式结束后就销毁了

引用折叠规则

  1. 所有的右值引用叠加到右值引用上仍然还是一个右值引用。(T&& && 变成 T&&

  2. 所有的其他引用类型之间的叠加都将变成左值引用。 (T& &, T& &&, T&& & 都变成 T&

  3. 对常量引用规则一致

  4. 示例

move()forward()

  • move() 的主要作用是将一个左值转为 xvalue(右值), 其实现本质上是一个 static_cast<T>

  • forward() 主要用于实现完美转发,其作用是将一个类型为(左值/右值)引用的左值,转化为它的类型所对应的值类型(左值/右值)

    觉得难以理解的话,就继续看下去吧

左值与右值的本质

  • 左值表示是“对象”(object),右值表示“”(value)——“对象”内存储着“值”

  • 左值 -> 右值的转换可看做“读取对象的值”(reading the value of an object)

  • 其他说法:

    • 左值是可以作为内存单元地址的值;右值是可以作为内存单元内容的值

    • 左值是内存中持续存储数据的一个地址;右值是临时表达式结果

左值、消亡值、纯右值

  • C++11 开始,表达式一般分为三类:左值(lvalue)、消亡值(xvalue)和纯右值(prvalue);

  • 其中左值和消亡值统称泛左值(glvalue);

    消亡值和纯右值统称右值(rvalue)。

右值引用的特点

右值引用延长了临时对象的生命周期

  • getI()getT() 都返回一个临时变量,但是 getT() 产生的临时变量不会在表达式结束后就马上销毁,而是会被“续命”——它的声明周期将和它的引用类型变量 t 一样长。

利用右值引用避免临时对象的拷贝和析构

  • 非右值引用,关闭返回值优化

  • 右值引用,关闭返回值优化

    利用常量引用也能避免临时对象的拷贝与析构 -> 常量(左值)引用

    返回值优化做的更彻底 -> 返回值优化 RVO

右值引用类型绑定的一定是右值,但 T&& 可能不是右值引用类型

当发生自动类型推断时,T&& 是未定的引用类型

  • T&& t 在发生自动类型推断时,是未定的引用类型

    • 比如模板元编程,auto 关键字等

    • 如果 t 被一个左值初始化,它就是一个左值;如果 t 被一个右值初始化,它就是一个右值

      ```Cpp

      template // 模板元编程

      void foo(T&& t) { } // 此时 T&& 不是右值引用类型,而是未定引用类型

    foo(10); // OK: 未定引用类型 t 绑定了一个右值

    int x = 10; foo(x); // OK: 未定引用类型 t 绑定了一个左值

    int&& p = x; // err auto&& t = x; // OK ```

  • 仅当发生自动类型推导时(模板编程,auto 关键字),T&& 才是未定引用类型

常量(左值)引用

  • 右值引用是 C++11 引入的概念

  • 在 C++11 前,是如何避免临时对象的拷贝和析构呢?——利用常量左值引用

  • 常量左值引用是一个“万能”的引用类型,可以接受左值、右值、常量左值和常量右值

  • 普通的左值引用不能接受右值

返回值优化 RVO

  • 利用右值引用可以避免临时对象的拷贝可析构

  • 但编译器的返回值优化(Return Value Optimization, RVO)做得“更绝”,直接回避了所有拷贝构造

    • 关闭编译器优化的结果

    • 开启编译器优化

    • 返回值优化并不是 C++ 的标准,是各编译器优化的结果,但是这项优化并不复杂,所以基本流行的编译器都提供

移动语义

深拷贝带来的问题

  • 带有堆内存的类,必须提供一个深拷贝构造函数,以避免“指针悬挂”问题

    所谓指针悬挂,指的是两个对象内部的成员指针变量指向了同一块地址,析构时这块内存会因被删除两次而发生错误

    ```Cpp class A { public: A(): m_ptr(new int(0)) { // new 堆内存 cout << "construct" << endl; }

    private: int* m_ptr; // 成员指针变量 };

    A getA() { return A(); }

    int main() { A a = getA(); return 0; }

    如果不关闭 RVO,只会输出 construct

  • 提供深拷贝能够保证程序的正确性,但会带来额外的性能损耗——临时对象也会申请一块内存,然后又马上被销毁了;如果堆内存很大的话,这个性能损耗是不可忽略的

  • 对于临时对象而言,深拷贝不是必须的

  • 利用右值引用可以避免无谓的深拷贝——移动拷贝构造函数

移动构造函数

  • 相比上面的代码,这里只多了一个移动构造函数——一般会同时提供拷贝构造与移动构造

  • 输出(关闭返回值优化)

    如果不关闭 RVO,只会输出 construct

  • 这里没有自动类型推断,所以 A&& 一定是右值引用类型,因此所有临时对象(右值)会匹配到这个构造函数,而不会调用深拷贝

  • 对于临时对象而言,没有必要调用深拷贝

  • 这就是所谓的移动语义——右值引用的一个重要目的就是为了支持移动语义

移动语义 与 move()

  • 移动语义是通过右值引用来匹配临时值,从而避免深拷贝

  • 利用 move() 方法,可以将普通的左值转化为右值来达到避免深拷贝的目的

    • 运行结果

  • STL 容器的移动语义

    • C++11 中所有的容器都实现了移动语义

move() 的本质

  • move() 实际上并没有移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用

  • 如果没有对应的移动构造函数,那么使用 move() 仍会发生深拷贝,比如基本类型,定长数组等

  • 因此,move() 对于含有资源(堆内存或句柄)的对象来说更有意义。

move() 的原型 TODO

c++11 中的 move 与 forward - twoon - 博客园

完美转发

  • 右值引用的引入,使函数可以根据值的类型(左值或右值)进行不同的处理

  • 于是又引入了一个问题——如何正确的传递参数,保持参数作为左值或右值的特性

  • 转发失败的例子:

    • 输出

    • 无论传入的是左值还是右值,val 都是一个左值

forward<T>() 实现完美转发

这里写的不够详细,有时间在整理

  • 在函数模板中,T&& 实际上是未定引用类型,它是可以得知传入的对象是左值还是右值的

  • 这个特性使其可以成为一个参数的路由,利用 forward() 实现完美转发

  • std::forward<T>() 可以保留表达式作为“对象”(左值)或“值”(右值)的特性

    <!-- ```Cpp

    int&& a = 1;

cout << &a; // OK: 虽然 a 是一个右值引用类型的变量,但它本身是一个左值 cout << &forward(a); // err: taking address of xvalue (rvalue reference)

  • 输出

  • 正确实现了转发

forward<T>()的原型 TODO

Reference

最后更新于

这有帮助吗?