字母l、r、x、gl和pr背后隐藏着什么?深入理解C++值类别
\n\n如果你曾经编写过C++代码并遇到类似 "cannot bind non-const lvalue reference to an rvalue" 的错误,那么你已经接触过这种“魔法”了。值类别——lvalue、rvalue、xvalue、glvalue和prvalue——不仅仅是学术分类。它们是移动语义(move semantics)和完美转发(perfect forwarding)的基础。
让我们一劳永逸地弄清楚每个字母的含义,如何在实践中区分它们,以及为什么没有这些知识就无法编写现代高效的C++代码。
\n\n简史:这五个术语从何而来?
\n\n最初,在C语言时代,只有两个概念:lvalue(左值——可以出现在赋值左侧的值)和rvalue(右值——出现在赋值右侧的值)。一切都很简单:变量是lvalue,数字是rvalue。
\n\n随着C++11和移动语义的出现,这种简单的划分不再适用。出现了rvalue引用(&&)以及区分“即将消亡”的临时对象的必要性。于是诞生了新的分类,C++标准通过五个类别来描述它。
请看层次结构(文本形式的图表):
\n\n expression\n / \\\n glvalue rvalue\n / \\ / \\\nlvalue xvalue prvalue\n\n记住这张图——这是理解一切的关键。
\n\nlvalue(左值)—— “持久”的对象
\n\nlvalue 是一个引用内存中有地址的对象的表达式。这样的对象在表达式结束后不会消失。你可以获取它的地址、修改它,并且它会继续存在。
\n\nlvalue示例:
\n- \n
- 变量名:
int x = 5; x是lvalue。 \n - 解引用的指针:
*ptr。 \n - 对变量的引用:
int& ref = x; ref。 \n - 字符串字面量:
"hello"(是的,它是lvalue,尽管是常量)。 \n
代码:
\nint main() {\n int a = 10;\n int* p = &a; // 取地址——可以,a是lvalue\n int& ref = a; // 左值引用\n\n // &(a + 1); // 错误!a+1是rvalue,没有地址\n return 0;\n}\n\nrvalue(右值)—— “一次性”的值
\n\nrvalue 是一个在内存中没有固定地址的表达式。它只在表达式计算时存在。你不能获取它的地址(少数例外情况除外)。
\n\nrvalue包括:
\n- \n
- 字面量(字符串字面量除外):
42、3.14f、true。 \n - 算术运算的结果:
a + b。 \n - 临时对象:
std::string("temp")。 \n - Lambda表达式。 \n
代码:
\nint foo() { return 5; }\n\nint main() {\n int x = 42; // 42是rvalue\n int y = x + 10; // x+10是rvalue\n // int* p = &(x + 10); // 错误!不能取rvalue的地址\n return 0;\n}\n\nprvalue(纯右值)—— “纯粹的”rvalue
\n\nprvalue 是C++98中经典的rvalue。它是一个没有名称且不绑定到内存的临时值。通常我们在口语中提到的rvalue就是指prvalue。
\n\nprvalue示例:
\n- \n
- 数字、枚举、运算符的结果。 \n
- 返回非引用的函数调用:
std::to_string(123)。 \n - 显式创建的临时对象:
MyClass()。 \n
xvalue(将亡值)—— “即将消亡”的对象
\n\n这里开始变得有趣。xvalue 是一个即将被销毁的对象,但其资源仍然可以“拯救”(移动)。这是mos