值类别

每一个C++表达式都可以用两种独立的特性来加以辨别:类型(type)和值类别(value category)。

C++98 中一个表达式的值类别可以是左值(lvalue)或者是右值(rvalue)。

C++11中一个表达式的基本值类别可以是纯右值(prvalue)、将亡值(xvalue)和左值(lvalue)。此外还有两个混合类别,泛左值(glvalue)和右值(rvalue)。

历史

C++11引入移动语义之前,值类别可以被简单的分为左值和右值。而随着移动语义的引入,值类别被重新定义,C++之父Bjarne曾在一篇文章中说过:一个C++表达式可以被分离出来两种完全独立的属性:

  • 拥有身份(has identity):即拥有地址,用户可以通过地址来确定一个表达式是否和另一个表达式指代同一个实体。
  • 可被移动(can be moved from):可以将这个表达式应用于具有移动语义的操作上,例如移动构造函数、移动拷贝函数等。

这里的拥有身份我觉得比较难理解,我自己的总结是,拥有身份即这个表达式所指代的对象具有地址也就是真实存在于内存中,而反之没有身份则表示其从语义的角度来说不应该在内存中有实体,如果有实体也是因为从实现的角度来说需要用它来初始化其他表达式,而在完成使命后它也应该被销毁。

举个例子,int i = 3 可以知道 i 这个对象肯定在内存中是有实体的,也就是上面所说的拥有身份。而 a + b 这个表达式从语义的角度来说只是代表对于两个变量进行求和,如果不将求和的结果赋值给其他对象则这个表达式毫无用处,也不应该在内存中存在实体。类似的还有 a++ 所指代的对象是 a++ 之前的值,其在内存中并没有实体。

可被移动表示一个对象从语义的角度来说其享有的资源可以被转移给其他对象,关于这个性质在后面有关移动语义的章节再做介绍。

拥有身份的表达式被分类为泛左值("generalized" lvalue),可被移动的表达式被分类为右值(rvalue)

而当这两种性质交叉起来,又可以得到四种结果:

  • 拥有身份但不能被移动的表达式被称为左值(lvalue)表达式
  • 拥有身份且可以被移动的表达式被称为将亡值(“eXpiring value")表达式
  • 没有身份但可以被移动的表达式被称为纯右值("pure" rvalue)表达式
  • 没有身份且不能被移动的表达式并不存在

下面这张图就展示上面所说的这种分类:

右值引用(C++11)

顾名思义,右值表达式的引用就是右值引用,左值表达式的引用就是左值引用。右值引用变量使用 && 来声明:int&& re = 3

右值引用可以延长一个临时变量的生命周期(const 左值引用也可以):

std::string s1 = "Hello";
const std::string& r1 = s1 + s1;  // r1 can't be modified
std::string&& r2 = s1 + s1;       // r2 can be modified
r2 += " world";

由于右值引用的这个特性,一个返回右值引用的函数调用被分类为将亡值表达式。

更重要的,如果一个函数有参数为左值引用和参数为右值引用的重载,则用右值(包括纯右值和将亡值)来调用会匹配到右值引用重载函数,用左值来调用会匹配到左值引用重载函数。这也就是C++11中的移动构造函数、移动赋值操作符以及其他具有移动语义的函数的基础。

const T& 作为参数可以同时匹配左值和右值。

每一种值类别的详细列表

下面这些表达式都是左值表达式:

  • 一个变量、函数、数据成员的名字
  • 一个返回左值引用的函数调用
  • a = ba += ba %= b 等所有赋值表达式
  • ++a --a
  • *p 解引用
  • arr[n] 其中 arr 是一个左值数组
  • ...

下面这些表达式都是纯右值表达式:

  • 一个字面值(除了字符串字面值)
  • 一个返回非引用的函数调用
  • a++ a--
  • a + ba - b 等内置算数表达式
  • &a 取地址表达式
  • 一个lambda表达式
  • ...

下面这些表达式都是将亡值表达式:

  • 一个返回右值引用的函数调用例如 std::move()
  • arr[n] 其中 arr 是一个右值数组
  • a.m 其中 a 是一个右值,m 是一个非引用的非静态数据成员

小结

这一章介绍了C++11中的值类别,其中大部分内容都提炼自cppreference。为了更好理解,我首先介绍了C++的一个表达式的两个完全独立的属性:拥有身份可移动。而根据一个表达式是否具有这两个属性引出了五种值类别:泛左值右值左值纯右值将亡值

我觉得两个属性相比而言是否可移动更加重要,因为这个属性扯出了C++11中一大片和移动相关的操作,也就是明白左值和右值的区别更加重要。而是否拥有身份其实知道不知道用处也不大,也就是泛左值和纯右值或者说将亡值和纯右值的区别并没那么易于理解和重要。

Last modification:November 22nd, 2021 at 10:33 pm