什么是右值(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” 右值,是右值但不是将亡值。例如,调用返回类型不是引用的函数的结果是纯右值,像
12
、7.3e5
或 true
这样的字面量的值也是纯右值。
区分不同值类别
- 左值(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; i; int* p_i = new int(7); *p_i; const int& r_I = 7; r_I; f(); return 0; }
|
- 将亡值(xvalues):表达式
E
属于将亡值类别,满足以下情况之一:- 调用返回类型为对返回对象类型的右值引用的函数的结果,无论是隐式还是显式调用。
1 2 3 4 5 6 7 8 9
| int&& f() { return 3; }
int main() { f(); return 0; }
|
- 转换为对象类型的右值引用。
1 2 3 4 5 6
| int main() { static_cast<int&&>(7); std::move(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; 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(); As&& rr_a = As(); rr_a; 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; } };
As f() { return As(); }
int main() { 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)));
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)); 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& b = a;
|
为什么需要这些新的值类别?
这些新的值类别主要是为了支持移动语义,允许在对象的生命周期即将结束时,将其资源移动到另一个对象,而不是进行昂贵的复制操作。同时,它们也使得 C++ 标准中的规则更加清晰和准确,避免了一些在处理右值引用时出现的问题。
如何区分一个表达式是左值还是右值?
可以使用 Scott Meyer 提出的经验法则:
- 如果可以取表达式的地址,那么该表达式是左值。
- 如果表达式的类型是左值引用(如
T&
或 const T&
等),那么该表达式是左值。 - 否则,该表达式是右值。概念上(通常实际上也是),右值对应于临时对象,如从函数返回的对象或通过隐式类型转换创建的对象。大多数字面量值(如
10
和 5.3
)也是右值。