左值、右值、move
原文: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:
- 字面量(除了字符串)
- 像 a++ 这样内置的后自增表达式(返回一个临时对象)
- 像 a+b 这样内置的运算、逻辑运算等
返回一个非引用类型
的函数的返回值- 强制转换成了非引用类型
- lambda 表达式
等等,更具体的分类可以看拓展阅读。
xvalue:
返回一个右值引用
的函数的返回值。比如 std::move(x)- 强制转换了右值引用
也就是说,通过使用 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