转载请保留以下声明
作者: 赵宗晟
C++11 之前 value categories 只有两类, lvalue 和 rvalue, 在 C++11 之后出现了新的 value categories, 即 prvalue, glvalue, xvalue. 不理解 value categories 可能会让我们遇到一些坑时不知怎么去修改, 所以理解 value categories 对于写 C++ 的人来说时比较重要的. 而理解 value categories 离不开一个概念 --move semantics. 了解 C++11 的人我相信都了解了 std::move, 右值引用和移动构造 / 移动复制等概念, 但是对 move semantics 这个概念的准确定义, 还是有很多人比较模糊的. 我想通过这篇文章谈一谈我对 value categories 和 move semantics 的理解. 首先从 move semantics 开始.
什么是 move semantics(移动语义)?
semantics 是来自语言学的一个概念, 翻译成中文就是 "语义". 说到计算机语言, 可能有很多人认为他是计算机科学下面的子门类. 实际上他是计算机科学和语言学的交叉科目, 里面有很多概念都来自语言学的内容, 也有语言学科班的学生之后去做编译的研究 / 工作. 所以我们先从自然语言入手, 通过类比能够更好地理解 move semantics. 下面有两个句子:
他是饭桶.
这是饭桶.
这两句话里面都有 "饭桶" 这个词, 但是两个句子中 "饭桶" 意思却不一样. 从语法上来看, 这俩都是 "<代词>是饭桶" 的形式, 只有代词不一样, 但句子意思却完全不一样了. 句子 1 的意思是骂一个人很没用, 句子 2 的意思是说明这个物体是盛饭的桶. 这个例子说明, 要理解一个单词的意思 (例如 "饭桶") 是要结合句中其他单词, 结合整个句子, 甚至要结合前后句理解.
而在 C++ 语言中也是类似的. 下面有两个 "句子"(语句):
- vec = vector<int>();
- vec = another_vec;
其中, vec 和 another_vec 都是 vector<int > 类型的变量.
这两个语句都是 "vec = XXXX;" 的形式, 但是语句 1 是把 XXXX 移动到变量 vec, 语句 2 是把 XXXX 拷贝给 vec. 两个语句中都有 "=" 运算符, 但是语句 1 中的意思是 "移动到", 语句 2 中的意思是 "拷贝给". 所以 "=" 运算符和整个句子的意思是由 XXXX 的类型决定的. 我们可以说语句 1 有移动的意思, 语句 2 有拷贝的意思, 或者说, 语句 1 中的 "=" 是移动的意思, 语句 2 中的 "=" 是拷贝的意思. 更正式地说, 语句 1 呈现了移动语义, 语句 2 呈现了拷贝语义, 语句 1 中的 "=" 呈现了移动语义, 语句 2 中的 "=" 呈现了拷贝语义. 用英文说则是, statement 1 displayed move semantics; statement 2 displayed copy semantics; operator= in statement 1 displayed move semantics; operator= in statement 2 displayed copy semantics.
其实说白了,"移动语义" 翻译成白话就是 "移动的意思".
怎么理解 5 种 value categories(值类别)?
C++ 中的每个表达式都有两种属性, 一个是类型 type, 另一个就是值类别 value category. 每个表达式的值类别一定属于且仅属于 prvalue (pure rvalue), xvalue, lvalue 三种中的一种. prvalue 和 xvalue 统称为 rvalue,xvalue 和 lvalue 统称为 glvalue (generalized lvalue), 如下图所示:
那么, prvalue,xvalue 和 lvalue 是怎么定义的?
其实所有表达式都有以下两种属性:
是否有 identity(同一性, 或者说 "有身份"): 是否可以与另一个表达式或对象比较, 判断是否是同一个实体. 比如, 如果有地址, 可以比较他们的地址相同;
是否可以移动: 如果出现在赋值, 初始化等语句中, 是否会使语句呈现移动语义.
于是有:
有 identity, 也可以移动的表达式为 xvalue;
有 identity, 但不能移动的表达式为 lvalue;
没有 identity, 但是可以移动的表达式为 prvalue;
至于没有 identity, 也不可以移动的表达式, 在实际应用中不存在这样的表达式, 也没必要有这样的表达式.
对于另外两种值类别, 我们可以这么总结:
有 identity 的表达式, 值类别为 glvalue;
可以移动的表达式, 值类别为 rvalue.
分析理解 C++ 标准中值类别的规则
举例来理解的话, 对于 xvalue 表达式, 有这样的规则:
如果一个表达式是函数调用或重载运算符表达式, 且其返回类型为右值引用, 例如 std::move(x), 那么这个表达式是 xvalue 表达式
对于这个规则, 我们可以这么理解. 首先返回一个对象, 肯定是要在栈上面预留内存空间的, 所以这个对象是由 identity 的. 返回类型是右值引用, 所以它会让使用这个表达式的语句呈现移动语义, 所以是可以动的. 因此, 这个表达式是 xvalue 表达式.
对于 xvalue 还有这样的规则
对象成员表达式, 即 "a.m", 如果 a 是右值且 m 是非引用类型的非静态数据成员, 则这个表达式是 xvalue 表达式
这条规则可以这么理解, a 是右值, 也就是可以移动, 那么对于 a 对象的一部分, m 也应当是可以移动的. 访问对象的 "." 运算符实际上是指针的位移运算, 既然要用指针, 那么肯定是有地址的. 因此, 这个表达式是 xvalue 表达式.
再比如:
对象成员表达式, 即 "a.m", 如果 m 是成员枚举符或非静态成员函数, 则这个表达式是 prvalue 表达式
不管是静态枚举符实际上是一个数字, 成员函数实际上是指向代码段的地址, 实际上也是一个数字, 而且都是在编译时期就决定了的数字. cpu 对这些数字操作时, 这些数字是直接放在指令内部的, 或者是放在寄存器中, 而不会在内存中存在, 所以他们是没有 identity 的. 所以这个表达式是 prvalue 表达式.
C++ 标准还定义了很多规则, 详细规定了哪些表达式是 prvalue, 哪些是 xvalue, 哪些又是 lvalue. 这些规则都可以用类似的方法分析并理解, 而不需要去死记硬背.
来源: https://www.cnblogs.com/zhao-zongsheng/p/value_categories_and_move_semantics.html