原文:https://zhuanlan.zhihu.com/p/265815272

C++98

历史上,我们把值分为两类,左值 ( lvalue ) 和右值 ( rvalue )。

右值,就是只能在等号右边的值,比如字面量。

左值,就是在等号左边出现的值,当然在等号右边也能出现。

 a = 1; // a 是左值, 1 是右值,这个 1 被称作字面量

但是这样的分类方法,在遇到 const int 这样的类型时,就发现一个 const int 既不能分为左值,也不能分类为右值。(有且只有初始化时才能在等号左边出现)

所以在 C 中,左值,就是表示了一个“对象”(object) 的值,比如一个变量,一个指针等等。在 C++98 中,还把函数变成了左值。

左值的特点就是,可以绑定上左值引用。如果要引用一个右值,那引用必须是一个常引用。

int a = 100; // a 是一个左值
int &b = a; // b 是一个左值引用, 对 b 操作任何事情完全就是对 a 操作

C++11

在 C++11 中引入了一种新的语义——移动语义。具体地说,就是可以移动构造,还有移动赋值。

移动语义有点像“废物利用”一样。如果采用了移动构造,你就可以把自己身上的数据移动给新的成员,避免了不必要的数据复制。比如要移动几千个 std::string 类型的成员,C++98 中只能够复制一份再删除一份,而 C++11 中,就可以改一下 std::string 内部指针的位置,很方便。

但是要注意,只有废物才能被利用,我们给这类“废物”一个名字,就叫 xvalue,x 的意思是“将要过期的”(expiring)。原来的右值 rvalue 中细分成为了“纯右值” prvalue (pure rvalue)

所以在 C++11 中,有了三种数据类型:

  • lvalue
  • xvalue
  • prvalue

其中 xvalue 和 prvalue 统称 rvalue;而 lvalue 和 xvalue 统称 glvalue.

---+++

我们举一些例子。

prvalue:

  1. 字面量(除了字符串)
  2. 像 a++ 这样内置的后自增表达式(返回一个临时对象)
  3. 像 a+b 这样内置的运算、逻辑运算等
  4. 返回一个非引用类型的函数的返回值
  5. 强制转换成了非引用类型
  6. lambda 表达式

等等,更具体的分类可以看拓展阅读。

xvalue:

  1. 返回一个右值引用的函数的返回值。比如 std::move(x)
  2. 强制转换了右值引用

也就是说,通过使用 std::move(x) 就可以把 x 的类型变成 xvalue,就可以调用移动构造函数了(如果实现了这个函数)。

(小提示:不要写出 return std::move(x); 这种语句,写 return x; 就行,不然会妨碍编译器优化。参考这里

---+++

进一步,我们可以抽象出来这两个判断法则:

准则 1:能不能分辨两个表达式指的是同一个物体。比如我们可以通过比较地址。

准则 2:能不能使用移动语义。比如看看能不能用调用移动构造函数。

  • 都满足,那就是 xvalue
  • 满足 1 不满足 2,就是 lvalue
  • 满足 2 不满足 1,就是 prvalue

满足 1 就统称为 glvalue,满足 2 的统称为 rvalue

分成这么多类,在绑定引用的时候就起了作用。比如不同的函数重载,一个 xvalue 优先会找右值引用,其次可能是常量左值的引用,这样就可以正确的发挥移动语义的作用了。

C++17

分类和 C++11 是一样的,但是语义上更加明确了。

  • glvalues:有自己地址的长寿对象
  • prvalues:为了初始化而用的短命对象
  • xvalue:资源已经不需要了,而且可以再利用的长寿对象

C++17 还引入了一些新的语法规定,有时候一个 prvalue 还可以 materialization 成 xvalue。当然,这些都不用太关心,写出问题了再说。

参考

https://en.cppreference.com/w/cpp/language/value_category