一、问题
c++的引用,是prvalue还是lvalue,还是xvalue?
● xvalue(eXpiring Value)。
为什么是 xvalue?
- prvalue 是临时值(rvalue 的一种)------右值引用的临时对象。
- lvalue 是可取地址的持久对象。
- xvalue 是"即将消亡的值"(即将要移动的临时对象),是 C++11 引入的第三类引用类型。
分类规则(根据 C++ 标准)
- lvalue:能取地址(&)、能作为左值(++、= 左侧等)。
- xvalue:右值引用 T&& 引用的临时对象,且即将要被移动(即将要析构)。
- prvalue:纯右值(临时表达式),不能取地址。
实际例子
cpp
class Test {
public:
Test() { cout << "ctor\n"; }
~Test() { cout << "dtor\n"; }
Test(const Test&) { cout << "copy ctor\n"; }
Test(Test&&) { cout << "move ctor\n"; }
};
void func(Test&& t) { /* t 是 xvalue */ }
Test getTemp() { return Test(); } // 返回临时对象 → xvalue
int main() {
Test t; // lvalue
func(t); // 错误:不能把 lvalue 绑定到 T&&
func(Test()); // ok ------ 临时对象是 xvalue
func(getTemp()); // ok ------ getTemp() 返回 xvalue
func(std::move(t)); // ok ------ 显式转为 xvalue
return 0;
}
总结一句话
- 普通临时对象(比如 Test()、getTemp() 返回值)→ xvalue。
- 右值引用参数接收的临时对象 → xvalue。
- 普通变量(左值)→ lvalue。
- 纯右值表达式(如 3 + 4、foo() + bar())→ prvalue。
如果你要区分它们,可以在代码里用 static_assert + type_traits 手动验证,或者在调试时打印 func、typeid。
二、对上面内容的分析
里面有多处关键错误 ,尤其是把 prvalue 和 xvalue 混淆了。
最根本的问题是:
T&、T&&是引用类型;lvalue、prvalue、xvalue 是表达式的值类别。两者不是同一个概念。
所以,"引用就是 xvalue""xvalue 是第三类引用类型"都是错误说法。
1. 原例中的正确分类
| 表达式 | 正确的值类别 | 原因 |
|---|---|---|
main 中的 t |
lvalue | 按变量名访问对象 |
func(Test&& t) 函数体中普通使用的 t |
lvalue | 即使声明类型是右值引用,直接使用参数名仍是左值表达式 |
Test() 或 Test{} |
prvalue | 构造该类型值的表达式 |
getTemp() |
prvalue | 函数按值返回 Test |
std::move(t) |
xvalue | 将对象表达式转换成 xvalue |
3 + 4 |
prvalue | 计算得到一个值 |
最容易弄错的一点:右值引用变量,按名字使用时,并不因此成为右值表达式。
例如:
cpp
Test a;
Test&& r = Test{};
// a 是 lvalue 表达式。
// r 也是 lvalue 表达式,尽管 r 的声明类型是 Test&&。
// func(a); // 错误:不能将这里的 lvalue 绑定到 Test&&
// func(r); // 同样错误!
func(Test{}); // 正确:实参表达式是 prvalue
func(getTemp()); // 正确:实参表达式是 prvalue
func(std::move(a)); // 正确:实参表达式是 xvalue
func(std::move(r)); // 正确:实参表达式是 xvalue
对于对象类型 T,还可以这样对照函数调用:
cpp
T f(); // 表达式 f() 是 prvalue
T& g(); // 表达式 g() 是 lvalue
T&& h(); // 表达式 h() 是 xvalue
2. 为什么 Test{} 是 prvalue,却能传给 Test&&?
因为 prvalue 和 xvalue 都属于右值(rvalue),这里的右值引用可以接受它们。
在 C++17 及以后:
cpp
func(Test{});
Test{} 原本是 prvalue 。为了绑定引用,会发生临时量实质化转换:创建临时对象,并产生指向它的 xvalue,再进行引用绑定。
"绑定过程中发生了转换",不等于"Test{} 原本就是 xvalue"。
而且,这里的引用绑定本身不会调用 Test 的移动构造函数。
3. 原文中的另外两条判断规则也不准确
xvalue 不代表对象马上析构,也不代表已经发生移动
cpp
Test a;
std::move(a); // 表达式是 xvalue,但这一句本身并没有移动任何东西
a 的生命周期不会因为 std::move(a) 而结束。std::move 本质上只是转换,使后续操作有机会选中移动构造、移动赋值等。
lvalue 不能简单定义成"能赋值、能取地址"
例如:
cpp
const int n = 10;
// n = 20; // 错误,但表达式 n 仍然是 lvalue
另外,位域表达式也可以是 lvalue,但不能对它使用内建取地址运算符。因此,"能不能赋值、取地址"不是可靠的完整分类标准。
4. 怎样用代码验证?
可以用 decltype((表达式))。对于这里的对象表达式:
- 得到
T&:表达式是 lvalue - 得到
T&&:表达式是 xvalue - 得到非引用类型
T:表达式是 prvalue
例如,使用 C++17:
cpp
#include <type_traits>
#include <utility>
struct Test {};
Test getTemp() { return Test{}; }
int main() {
Test&& r = Test{};
// 注意下面两行的区别:
static_assert(std::is_same_v<decltype(r), Test&&>); // r 的声明类型
static_assert(std::is_same_v<decltype((r)), Test&>); // r 表达式是 lvalue
static_assert(std::is_same_v<decltype((Test{})), Test>);
static_assert(std::is_same_v<decltype((getTemp())), Test>);
static_assert(std::is_same_v<decltype((std::move(r))), Test&&>);
}
这里额外的括号很重要:decltype(r) 对未加括号的变量名有特殊规则,取得的是声明类型;decltype((r)) 才按表达式的值类别推导。
原文说通过 typeid 或打印 __func__ 来判断值类别,也不可靠,它们不能直接区分这三类。
一句话记忆:T&& 是引用类型;在上述例子中,r 是 lvalue,std::move(r) 是 xvalue,Test{} 是 prvalue。