左值右值

在 C++ 中,每一个表达式都有两个独立的属性:类型和值类别,类型包含了这个表达式求值结果的内存布局信息:这个值有哪些字段,每个字段多长(字段类型、偏移量),如果这个类型是个 class, 则类型还包含了关于这个对象能做什么的信息(方法)。

值类别 (value category) 是这样的,最早在 C 语言的一条赋值语句中,等号左边的表达式称为左值,这个称呼习惯对我们理解什么是左值有帮助:左值是有确定内存地址的表达式,换言之,如果一个表达式 expr 是左值 (lvalue), 那么对它做取地址运算 &expr 是合法的。

如果一个表达式不属于左值的范畴,那么可以叫他非左值 (non-lvalue), 而到了 C++98, 非左值也被叫做右值 (rvalue).

如果我们声明一个 int 型变量并且给它初始化,那么在编译器看来,这个变量也就有了一个确定的、与之对应的内存地址:

int x = 1; // 声明一个 int 型变量 x 并且通过 copy-initialization 的方式初始化其值为 1
int *ptrX = &x; // 可以取地址

void foo1(int *) { /** */ }
foo1(&x); // 可以取地址然后调用 foo1 函数,这个函数接受一个内存地址,这个内存地址是一个 int 型对象的内存地址

void foo2(int &) { /** */ }
foo2(x);  // 可以绑定到函数的左值引用 (lvalue reference) 形参

void foo3(int &&) { /** */ }
foo3(x);  // 不允许,表达式 x 是一个左值,而 foo3 的形参是一个右值引用,只能被 bind 到一个右值

void foo4(int &&x) {
    foo3(x);  // Error: foo3 expects an rvalue,
              // x is a lvalue, even though x has type of rvalue reference to int. 
}

这里,表达式 x 是一个左值,因为这个表达式只由一个变量名 x 组成,而这个 x 被声明为一个 int 型变量,它的内存地址是确定的。

在下列代码中

void f(int&&) { /** */ }
f(1 + 1); // Ok: expr `1 + 1` is an rvalue, it can bind to f's 1st parameter
int *ptrExpr = &(1+1); // Error: expr `1+1` is an rvalue, can not take address of rvalue of type int.

表达式 1 + 1 是一个右值 (rvalue),因为编译器通常来说不会为 1 + 1 这个表达式分配一个固定的内存地址来存储其求值结果,所以它被叫做右值。

在 C++11 之前,C++ 语言中「左值」和非「左值」的含义与 C 语言的是类似的,在 C++11 之后,表达式的值类别被进一步细化,细分为了三个子类别:lvalue(左值),xvalue(亡值,eXpiring value) 和 prvalue(纯右值),其中 lvalue 和 xvalue 统称 glvalue(广义左值,general lvalue),而 xvalue 和 prvalue 统称 rvalue,xvalue 同时具备 glvalue 和 rvalue 的属性,lvalue 具备 glvalue 的属性,prvalue 也具备 rvalue 的属性。

具体来说,lvalue, prvalue 和 xvalue 是这么定义的,首先理解两个概念:

  1. 具备身份(has an identity):对于一个表达式,如果存在某种方式能够判断这个表达式指代的实体和另外一个表达式指代的实体是否同一个,则我们说这个表达式的身份是确定的。

例如,一个变量名表达式,或者说一个函数名表达式,这样的表达式它背后的实体是确定的:因为,拿到一个变量名表达式 a, 如果还有另外一个变量名表达式 b, 则我们可以判断 ab 是不是同一个实体,简单来说只要取地址判断就可以了:&a == &b.

对于函数名而言也是一样,因为函数名相当于一个指针,设 f1f2 是两个函数名(或者指针),则如果 f1 == f2true 就说明 f1f2 是同一个函数,当 f1 == f2 时,将表达式 f1(expr...) 替换成 f2(expr...) 不会改变代码的语义;

  1. 是可移动的(can be moved from):能够作为实参用来调用移动构造器 (move constructor), 移动赋值操作符(move-assignment operator), 或者接受右值引用的函数重载;

假如说一个 class Foo 声明了这两个构造器(互为重载):explicit Foo(const Type &)explicit Foo(Type &&), 前者通常被叫做 copy constructor, 后者叫做 move constructor, 如果表达式 expr 是一个右值,用 expr 来构造一个 Foo 对象时,被调用的就会是 move constructor, 这时我们说 expr 是可以移动的(可移的)。

另外,对于一般的重载函数:假如有两个重载 Type foo(int &)Type foo(int &&) , 表达式 expr 是可移的,则 foo(expr) 调用的是接受右值引用的那个重载,也就是形参类型为 int && 的那个。

那么基于这两个概念(具备身份和可移),我们可以具体地说 lvalue, xvalue 和 prvalue 的定义: