C++ 左/右值及其引用 论述

说明: 本文探讨的是 C++11 以来的值类别

关于左值和右值,在不对其进行详细的划分时,简单的分类方法包括

  1. 左值持久,右值短暂
  2. 能取得地址得通常是左值,反之通常是右值(这一方法启示我们一个表达式的类型与其是左值还是右值无关,即相同类型的表达式既可以是左值也可以是右值)

右值引用是必须绑定到右值的引用,通过&&获取右值引用
左值引用如果想绑定要右值上,只能通过const,例如

int i = 1;
int &r = i;           // 正确,i是一个左值,可以采用左值引用
int &r = i * 2;       // 错误,i*2是一个右值,无法直接采用左值引用
const int &r = i * 2; // 正确
int &&r = i * 2;      // 正确,i*2是一个右值,可以采用右值引用

C++11以前,右值被认为是无用的资源,右值引用引入的目的是为了延长用来初始化对象的生命周期(左值的生命周期和作用域相关,无需延长)

int x = 20;   // 左值
int&& rx = x * 2;  // x*2的结果是一个右值,rx延长其生命周期
int y = rx + 2;   // 可以重用右值:42
rx = 100;         // 一旦初始化一个右值引用变量,该变量就成为了一个左值,可以被赋值

以上代码一方面展示了右值引用延长生命周期的效果,同时引出了右值引用一个重要的特点,即初始化之后的右值引用将变成一个左值,如果是non-const还可以被赋值

关于"左值持久,右值短暂"这种说法,存在一个第一次看让人感到惊讶的例子,即

不能将一个右值引用绑定到一个右值引用类型的变量上

int &&r1 = 42; // 正确,字面值常量是右值
int &&r2 = r1; // 错误,表达式r1(变量可以看作只有一个运算对象而无运算符的表达式)是左值

r1虽然是右值引用,但其本质是一个变量,是一个持久值,因此其属于左值,不能直接被右值引用绑定

不能直接将右值引用绑定到左值上,但左值引用可以通过const实现绑定右值,因此C++11引入了新标准库函数move用于显示地将左值转换为对应右值引用类型,实现用右值类型绑定左值

#include <utility>
int &&r1 = 42;            // 42为右值,r1是右值引用类型,本身属于左值
int &&r2 = std::move(r1); // 正确,move将左值转换为右值引用

move函数有几点注意事项

  1. 函数所在头文件为 <utility>
  2. 由于右值引用能接受任意类型参数,为了避免与名为move且单参数的自定义move函数冲突,move并不提供using命名空间声明,必须使用std::move而非move
  3. 引用折叠的存在,使得右值引用可以接受左值和右值,因此std::move可以接受任意参数
  4. 对于上述代码,调用move意味着不能对移后源对象r1的数值做任何假设,不能再读取它的数值,但是可以进行赋值和销毁操作

虽然说move函数叫做移动,但其只是起到转换作用(从实际效果来看,称其为rvalue_cast似乎更合适),其并没有移动什么东西。对于目前"移动"的名称,更可能是因为其告知了编译器某个对象更加适合移动,所以才称其为move

同时有一个易错点是:使用了std::move并不一定会去利用移动构造函数。在下面的例子中,虽然初始化成员属性时采用了右值,但是由于形参string采用了const,因此实际调用的是拷贝构造而非移动构造函数,因此在创建希望能移动对象时,不要声明它们为const

// 
class Annotation {
public:
    explicit Annotation(const std::string text)
    :value(std::move(text))    //“移动”text到value里;这段代码执行起来
    {}

private:
    std::string value;
};

// std::string 类定义
class string {
public:
    string(const string& rhs);  //拷贝构造函数
    string(string&& rhs);       //移动构造函数
};

关于左值引用和右值引用能够接受参数的范围

  1. 当模板参数为左值引用时,只能传递给它的一个左值的实参。
  2. 当模板参数为const左值引用时,传递给它的一个左值的实参/临时对象、字面值等(右值)
  3. 当模板函数参数是一个右值引用时(准确来说是通用引用,形式与右值引用相同),传递给它的实参可以是任意类型

上面提到了"通用引用",通用引用本质上是特定上下文的右值引用,通过类型推导区分左右值得出上下文,并且会发生引用折叠。
发生通用引用有4种情况

  1. 模板实例化
  2. auto类型推导
  3. typedef与别名声明的创建和使用
  4. decltype

通用引用有2个特点

  1. 其形式与右值引用相同,这是由于通用引用本身就是特殊场景下的右值引用
  2. 可接受任意类型的实参
    关于第2点,实际是C++两种机制叠加的效果,一是类型推导,二是引用折叠
    使用通用引用时,左值实参会被推导为左值引用,右值实参会被推导为非引用,示例如下
Widget widgetFactory();     //返回右值的函数
Widget w;                   //一个变量(左值)
func(w);                    //用左值调用func;T被推导为Widget&
func(widgetFactory());      //用右值调用func;T被推导为Widget

理解引用折叠,需要首先明确以下原则

C++中引用的引用是非法的

此原则并不难理解,如果可以声明引用的引用,那么理论上就可以做到无限引用

int x;
int& &rx = x; // 错误,不能声明引用的引用

在此原则基础上,考虑把一个左值赋值给一个通用引用,在进行类型推导后会被推导为左值引用,此时出现的就是引用的引用,例如以下示例代码

template<typename T>
void func(T&& param);       //同之前一样

func(w);                    //用左值调用func;T被推导为Widget&

以上代码在经历类型推导后,得到的结果是void func(Widget& && param);
显然这是非法的,因此编译器会进行引用折叠
经历了类型推导和引用折叠后,对于原始传入的左值,最终形成了左值引用;对于原始传入的右值,最终形成了右值引用

Reference

posted @ 2023-06-19 17:05  0x7F  阅读(13)  评论(0编辑  收藏  举报