什么是右值(rvalues)、左值(lvalues)、将亡值(xvalues)、泛左值(glvalues)和纯右值(prvalues)?

什么是右值(rvalues)、左值(lvalues)、将亡值(xvalues)、泛左值(glvalues)和纯右值(prvalues)?

技术背景

ISOC++11(正式名称为 ISO/IEC 14882:2011)是 C++ 编程语言标准的最新版本,它引入了一些新特性和概念,如右值引用、xvalue、glvalue、prvalue 表达式值类别以及移动语义。这些新的表达式值类别概念的引入,与右值和左值引用密切相关,并且右值可以传递给非 const 右值引用。为了理解这些概念,我们需要深入探讨各个值类别的定义和特点。

实现步骤

理解基本概念

  • 左值(lvalue):历史上,左值可以出现在赋值表达式的左侧,它指定一个函数或对象。例如,如果 E 是指针类型的表达式,那么 *E 是一个左值表达式,它引用 E 所指向的对象或函数;调用返回类型为左值引用的函数的结果也是左值。
  • 将亡值(xvalue):“eXpiring” 值,也引用一个对象,通常是在其生命周期即将结束时,以便其资源可以被移动。它是涉及右值引用的某些类型表达式的结果,例如调用返回类型为右值引用的函数的结果就是将亡值。
  • 泛左值(glvalue):“generalized” 左值,是左值或将亡值。
  • 右值(rvalue):历史上,右值可以出现在赋值表达式的右侧,它是将亡值、临时对象或其子对象,或者是与对象无关的值。
  • 纯右值(prvalue):“pure” 右值,是右值但不是将亡值。例如,调用返回类型不是引用的函数的结果是纯右值,像 127.3e5true 这样的字面量的值也是纯右值。

区分不同值类别

  • 左值(lvalues):表达式 E 属于左值类别,当且仅当 E 引用一个已经具有身份(地址、名称或别名)的实体,使其可以在 E 之外被访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int i = 7;

const int& f() {
return i;
}

int main()
{
std::cout << &"www" << std::endl; // 表达式 "www" 是左值表达式,因为字符串字面量是数组,每个数组都有地址
i; // 表达式 i 是左值表达式
int* p_i = new int(7);
*p_i; // 表达式 *p_i 是左值表达式
const int& r_I = 7;
r_I; // 表达式 r_I 是左值表达式
f(); // 表达式 f() 是左值表达式
return 0;
}
  • 将亡值(xvalues):表达式 E 属于将亡值类别,满足以下情况之一:
    • 调用返回类型为对返回对象类型的右值引用的函数的结果,无论是隐式还是显式调用。
1
2
3
4
5
6
7
8
9
int&& f() {
return 3;
}

int main()
{
f(); // 表达式 f() 属于将亡值类别
return 0;
}
- 转换为对象类型的右值引用。
1
2
3
4
5
6
int main()
{
static_cast<int&&>(7); // 表达式 static_cast<int&&>(7) 属于将亡值类别
std::move(7); // std::move(7) 等价于 static_cast<int&&>(7)
return 0;
}
- 指定非引用类型的非静态数据成员的类成员访问表达式,其中对象表达式是将亡值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct As
{
int i;
};

As&& f() {
return As();
}

int main()
{
f().i; // 表达式 f().i 属于将亡值类别
return 0;
}
- 第一个操作数是将亡值,第二个操作数是数据成员指针的成员指针表达式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <functional>

struct As
{
int i;
};

As&& f() {
return As();
}

int main()
{
f(); // 表达式 f() 属于将亡值类别
As&& rr_a = As();
rr_a; // 表达式 rr_a 属于左值类别,因为它引用一个命名的右值引用对象
std::ref(f); // 表达式 std::ref(f) 属于左值类别,因为它引用一个右值引用函数
return 0;
}
  • 纯右值(prvalues):表达式 E 属于纯右值类别,当且仅当 E 既不属于左值类别也不属于将亡值类别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct As
{
void f() {
this; // 表达式 this 是纯右值表达式
}
};

As f() {
return As();
}

int main()
{
f(); // 表达式 f() 属于纯右值类别
return 0;
}
  • 混合值类别
    • 右值(rvalues):表达式 E 属于右值类别,当且仅当 E 属于将亡值类别或纯右值类别。这意味着表达式 E 引用一个尚未具有使其可以在 E 之外被访问的身份的实体。
    • 泛左值(glvalues):表达式 E 属于泛左值类别,当且仅当 E 属于左值类别或将亡值类别。

核心代码

以下是使用 decltype 说明符来测试值类别的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <type_traits>

#define IS_XVALUE(X) std::is_rvalue_reference<decltype((X))>::value
#define IS_LVALUE(X) std::is_lvalue_reference<decltype((X))>::value
#define IS_PRVALUE(X) !std::is_reference<decltype((X))>::value

#define IS_GLVALUE(X) (IS_LVALUE(X) || IS_XVALUE(X))
#define IS_RVALUE(X) (IS_PRVALUE(X) || IS_XVALUE(X))

void doesNothing() {}
struct S
{
int x{ 0 };
};

int main()
{
int x = 1;
int y = 2;
S s;

static_assert(IS_LVALUE(x));
static_assert(IS_LVALUE(x += y));
static_assert(IS_LVALUE("Hello world!"));
static_assert(IS_LVALUE(++x));

static_assert(IS_PRVALUE(1));
static_assert(IS_PRVALUE(x++));
static_assert(IS_PRVALUE(static_cast<double>(x)));
static_assert(IS_PRVALUE(std::string{}));
static_assert(IS_PRVALUE(throw std::exception()));
static_assert(IS_PRVALUE(doesNothing()));

static_assert(IS_XVALUE(std::move(s)));
// 下一个在 gcc 8.2 中不起作用,但在 gcc 9.1 中可以。Clang 7.0.0 和 msvc 19.16 正常工作。
// static_assert(IS_XVALUE(S().x));

return 0;
}

最佳实践

使用 std::move 进行资源转移

当你有一个左值非临时数据,在一个作用域中已经使用完毕,但想在另一个作用域中复用其资源时,可以使用 std::move 将其转换为右值引用,以便调用移动构造函数或移动赋值运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>

class MyClass {
public:
MyClass() {
std::cout << "Default constructor" << std::endl;
}
MyClass(const MyClass& other) {
std::cout << "Copy constructor" << std::endl;
}
MyClass(MyClass&& other) noexcept {
std::cout << "Move constructor" << std::endl;
}
};

int main() {
MyClass obj1;
std::vector<MyClass> vec;
vec.push_back(std::move(obj1)); // 使用 std::move 调用移动构造函数
return 0;
}

避免不必要的复制

理解值类别有助于避免不必要的复制操作,提高程序性能。例如,在函数返回对象时,如果返回的是临时对象,应该返回纯右值,让编译器可以进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

class MyClass {
public:
MyClass() {
std::cout << "Default constructor" << std::endl;
}
MyClass(const MyClass& other) {
std::cout << "Copy constructor" << std::endl;
}
MyClass(MyClass&& other) noexcept {
std::cout << "Move constructor" << std::endl;
}
};

MyClass createObject() {
return MyClass(); // 返回纯右值
}

int main() {
MyClass obj = createObject();
return 0;
}

常见问题

右值引用变量是右值吗?

不是。右值引用变量本身是左值。虽然右值引用可以绑定到右值,但一旦绑定,它就有了名称,具有了身份,所以成为了左值。例如:

1
2
3
int&& a = 3;
int&& c = a; // 错误:不能将 'int' 左值绑定到 'int&&'
int& b = a; // 编译通过

为什么需要这些新的值类别?

这些新的值类别主要是为了支持移动语义,允许在对象的生命周期即将结束时,将其资源移动到另一个对象,而不是进行昂贵的复制操作。同时,它们也使得 C++ 标准中的规则更加清晰和准确,避免了一些在处理右值引用时出现的问题。

如何区分一个表达式是左值还是右值?

可以使用 Scott Meyer 提出的经验法则:

  • 如果可以取表达式的地址,那么该表达式是左值。
  • 如果表达式的类型是左值引用(如 T&const T& 等),那么该表达式是左值。
  • 否则,该表达式是右值。概念上(通常实际上也是),右值对应于临时对象,如从函数返回的对象或通过隐式类型转换创建的对象。大多数字面量值(如 105.3)也是右值。

什么是右值(rvalues)、左值(lvalues)、将亡值(xvalues)、泛左值(glvalues)和纯右值(prvalues)?
https://119291.xyz/posts/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues/
作者
ww
发布于
2025年5月28日
许可协议