值的类别
在 C++ 中,有三种主要类别的值:
- 左值(left value, abbr. lvalue)
- 将亡值(expiring value, abbr. xvalue)
- 纯右值(pure rvalue, abbr. prvalue)
要区分值的类别,可以根据值的如下两个原则进行判断:
- 有标识(has identity):表达式的值是否能够通过直接或间接获得的地址标识。
- 可移动(can be moved from):具有移动语义,其资源可以被复用。
| 值类别 | 有标识 | 可移动 |
|---|---|---|
| 左值 | 是 | 否 |
| 将亡值 | 是 | 是 |
| 纯右值 | 否 | 是 |
下面的代码展示了如何通过模板特化来判断一个类型的值类别:
template <class T> struct is_prvalue : std::true_type {};
template <class T> struct is_prvalue<T&> : std::false_type {};
template <class T> struct is_prvalue<T&&> : std::false_type {};
template <class T> struct is_lvalue : std::false_type {};
template <class T> struct is_lvalue<T&> : std::true_type {};
template <class T> struct is_lvalue<T&&> : std::false_type {};
template <class T> struct is_xvalue : std::false_type {};
template <class T> struct is_xvalue<T&> : std::false_type {};
template <class T> struct is_xvalue<T&&> : std::true_type {};
左值
典型的左值包括:
- 变量
- 函数
- 由函数返回的左值引用
int a = 42;
void foo() {}
int& bar() { return a; }
int main()
{
static_assert(is_lvalue<decltype(a)>::value);
static_assert(is_lvalue<decltype(foo)>::value);
static_assert(is_lvalue<decltype(bar())>::value);
}
其中,比较难以理解的是 foo 为何是左值,毕竟下面的代码是合法的:
using function_t = void (*)();
function_t f = foo;
首先可以肯定的是 foo 具有标识,因此肯定是左值或者将亡值。左值意味着 foo 所指示的函数实体不可移动。foo 所指示的实体,实际上是 .text 段中的只读程序代码,其所有者是 foo 符号本身,我们无法将该实体的所有权转移给其他符号,因此 foo 作为一个符号,其所指示的实体是不可移动的,从而 foo 是左值。
那为何我们仍然可以将其赋值给一个函数指针呢?这就涉及到赋值过程中的类型退化(type decay)。在赋值过程中,foo 的语义转变成了函数的入口地址,而函数的入口地址是在编译期就确定的常量值,因此是纯右值。
将亡值
将亡值有标识,且可移动。典型的将亡值包括:
- 由函数返回的将亡值引用
- 通过
std::move获得的值
struct Foo
{
int data;
};
Foo&& get_foo() { return Foo{42}; }
int main()
{
static_assert(is_xvalue<decltype((get_foo()))>::value);
static_assert(is_xvalue<decltype((get_foo().data))>::value);
Foo f{42};
static_assert(is_xvalue<decltype((std::move(f)))>::value);
}
纯右值
纯右值没有标识,且可移动。直观来理解,一个东西没有所有者,自然可以被任意移动。
典型的纯右值包括:
- 字面值常量
- 由函数返回的纯右值
struct Foo
{
int data;
};
int get_foo() { return Foo{42}; }
int main()
{
static_assert(is_prvalue<decltype(42)>::value);
static_assert(is_prvalue<decltype(get_foo())>::value);
}
考虑下面的例子:
int&& n = 1;
decltype(n) // int&&
decltype((n)) // int&
1 本身是纯右值,因此用 int&& 来接收它是合法的。而一旦 n 绑定到了 1 这个值上,这个纯右值被移动到了内存的某个位置,并且被 n 所标识。
decltype(n) 返回的是 n 的类型,即 int&&。而 decltype((n)) 是 n 放在表达式中进行计算后得到的类型,因此返回的是 n 所指示的实体的类型,由于 n 现在具有标识,因此其类型变成了左值引用 int&。
这带给我们一个很重要的启示:一定要注意区分标识符本身和标识符所指示的实体。
混合类型
C++ 中的左值和右值实际上并没有上面那样严格,而是介于三种类型之间的混合类型:
- 泛化左值(generalized lvalue, abbr. glvalue):包括左值和将亡值。
- 右值(rvalue):包括将亡值和纯右值。
struct Foo
{
int data;
};
Foo&& get_foo() { return Foo{42}; }
int main()
{
const Foo& foo1 = get_foo(); // 合法,get_foo() 是泛化左值
Foo&& foo2 = get_foo(); // 合法,get_foo() 是右值
}