本文主要记录左值引用与 C++11 中引入的右值引用,以及右值引用相关的应用:移动语义与完美转发。
左值与右值
左值与右值的区分标准在于能否获取其在内存中的地址。
左值(lvalue):可以获取地址的对象。
右值(rvalue):无法获取地址的对象,如常量、函数返回值、Lambda 表达式等。
1 | int i = 10; // i 是左值,10 是右值 |
右值虽然无法获取地址,但是不代表不可以改变其值;当定义了右值对象的右值引用时,就可以获取对象的地址,也就可以改变对象的值了。
左值引用与右值引用
传统的 C++ 引用被称为左值引用,用法如下:
1 | int i = 10; |
C++ 11 中引入了右值引用的概念,当右值引用绑定到右值时,右值被存储到特定位置,右值引用指向该特定位置,用法如下:
1 | int &&rvalue_ref = 10; |
右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。
右值引用的应用场景:移动语义、完美转发。
const 修饰的左值引用
定义一个左值引用,将其值设置为一个常量值,则会报错:
1 | int &i = 10; // ERROR |
变量 i 是一个左值引用,而常量 10 是一个右值,因此无法将左值引用绑定到一个右值上。
但如果 i 是一个用 const 修饰的左值引用,那么是可以绑定到右值上的:
1 | const int &i = 10; |
常量左值引用 (const &) 可以接受常量左值 (const lvalue),非常量左值 (lvalue),右值 (rvalue)。
在早期的 C++ 中,引用并无左右之分,引入了右值引用后,传统的引用才被称为左值引用。
因此左值引用其实可以绑定任何对象,这也是为什么 const 修饰的左值引用可以绑定常量值。
1 | int i = 10; |
移动语义
右值引用是用来支持移动语义的。移动语义可以将资源从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 的性能。
移动语义是和拷贝语义相对的,就像文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度会比剪切慢很多。
实例分析
定义 point 类:
1 | class point { |
拷贝构造函数:
1 | point(const point &pt) { |
需要深拷贝指针,浅拷贝可能会导致多次释放内存,从而引起错误。
移动构造函数:
1 | point(point &&pt) { |
临时对象的指针需要置空,防止临时对象释放资源时,影响已经转移给新对象的资源。
拷贝赋值运算符重载:
1 | point &operator=(const point &pt) { |
移动赋值运算符重载:
1 | point &operator=(point &&pt) { |
完美转发
首先看两条规则,这两条规则决定了引用类型参数的函数传参方式。
引用折叠规则
C++ 中存在“引用的引用”(reference to reference),分为以下 4 种情况:
- lvalue reference to lvalue reference(左值引用绑定到左值引用)
- lvalue reference to rvalue reference(左值引用绑定到右值引用)
- rvalue reference to lvalue reference(右值引用绑定到左值引用)
- rvalue reference to rvalue reference(右值引用绑定到右值引用)
但是由于 C++ 中不允许“引用的引用”的存在,所以编译器会根据引用折叠规则(Reference Collapsing),将这 4 种情况转换为单引用(Single Reference)。其中,情况 1、2、3 会被转化为左值引用,情况 4 会被转化为右值引用。
模板函数对右值引用参数(T &&)的类型推导规则
模板函数定义如下:
1 | template <typename T> |
向一个模板函数传递一个左值实参,且该模板函数的对应形参为右值引用时,编译器会把该实参推导为左值引用。具体情况见下表:
实参类型 | T 的类型 | 形参类型 |
---|---|---|
具名对象(左值) | T & | T & |
不具名对象(右值) | T | T && |
具名左值引用(左值) | T & | T & |
不具名左值引用(左值) | T & | T & |
具名右值引用(左值) | T & | T & |
不具名右值引用(右值) | T | T && |
实例分析
定义 point 类:
1 | class point { |
定义下面的两个函数:
1 | template <typename T> |
调用函数 func_main 传入一个 point 类的临时对象(右值):
1 | func_main(point()); |
得到输出如下:
1 | constructor default // 默认构造临时对象 |
由于 func_main 函数中的 t 是具名右值引用,因此 t 是左值,传入 func_sub 函数的实参也就是左值了,而 func_sub 函数的形参是值传递形式,所以会调用 point 类的拷贝构造函数。
如果想要调用 point 类的移动构造函数,则需要传入右值。完美转发函数 std::forward 就提供了这样的功能(完美地保留变量的左右值属性并进行转发)。我们对 func_main 进行一定的修改:
1 | template <typename T> void func_main(T &&t) { |
这样一来,右值引用所绑定的变量的右值属性得到了保留,并传入 func_sub 函数,从而调用 point 类的移动构造函数对 func_sub 的形参进行构造。
再次运行程序,可以得到输出如下:
1 | constructor default // 默认构造临时对象 |