C++11的value category(值类别)以及move semantics(移动语义)

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. 这是饭桶。

这两句话里面都有“饭桶”这个词,但是两个句子中“饭桶”意思却不一样。从语法上来看,这俩都是“<代词>是饭桶”的形式,只有代词不一样,但句子意思却完全不一样了。句子1的意思是骂一个人很没用,句子2的意思是说明这个物体是盛饭的桶。这个例子说明,要理解一个单词的意思(例如“饭桶”)是要结合句中其他单词,以及整个句子的。

在C++语言中也是类似的。下面有两个“句子”(语句):

  1. vec = vector<int>();
  2. 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是怎么定义的?

其实所有表达式都有以下两种属性:

  1. 是否有identity(同一性,或者说“有身份”):是否可以与另一个表达式或对象比较,判断是否是同一个实体。比如,如果有地址,可以比较他们的地址相同;
  2. 是否可以移动:如果出现在赋值,初始化等语句中,是否会使语句呈现移动语义。

于是有:

  1. 有identity,也可以移动的表达式为xvalue表达式;
  2. 有identity,但不能移动的表达式为lvalue表达式;
  3. 没有identity,但是可以移动的表达式为prvalue表达式;

至于没有identity,也不可以移动的表达式,在实际应用中不存在这样的表达式,也没必要有这样的表达式。

对于另外两种值类别,我们可以这么总结:

  1. 有identity的表达式,值类别为glvalue;
  2. 可以移动的表达式,值类别为rvalue。

分析理解C++标准中决定值类别的规则

C++标准给出了一系列规则,来规定哪些表达式有哪种值类别。我们可以结合上面给出的值类别定义去理解这些规则。举个例子,对于xvalue表达式,有这样的规则:

如果一个表达式是函数调用或重载运算符表达式,且其返回类型为右值引用,例如 std::move(x),那么这个表达式是xvalue表达式

对于这个规则,我们可以这么理解:首先,如果要返回一个对象,肯定是要在栈上面预留内存空间的,所以这个对象是有identity的。第二,返回类型是右值引用,所以它会让使用这个表达式的语句呈现移动语义,所以是可移动的。因此,这个表达式是xvalue表达式。

对于xvalue还有这样的规则

对象成员表达式,即"a.m",如果 a 是右值且 m 是非引用类型的非静态数据成员,则这个表达式是xvalue表达式

这条规则可以这么理解:首先,a是右值,也就是可以移动,那么作为a对象的一部分,m也应当是可以移动的。第二,访问对象的“.”运算符实际上是计算地址偏移,既然有地址,那么肯定是有identity的。因此,这个表达式是xvalue表达式。

再比如:

对象成员表达式,即"a.m",如果 m 是成员枚举符或非静态成员函数,则这个表达式是prvalue表达式

枚举符在编译后其实就是一个数字;成员函数在编译后实际上是指向代码段的地址,实际上也是一个数字。这两个数字都是在编译时期就决定了的数字。cpu使用这些数字时,这些数字是直接放在指令内部或者是放在寄存器中的,不会放在内存中,所以他们是没有identity的。其实换个角度想,因为他们只是一个值,不是变量,所以没有identity也是很合理的。因此,这个表达式是prvalue表达式。

C++标准还定义了很多这样的规则,都可以用类似的方法分析并理解,而不需要去死记硬背。

 

posted @ 2018-04-19 17:55  赵宗晟  阅读(1537)  评论(1编辑  收藏  举报