结论: identity 不是一份需要单独存放的"身份信息",而是指"这个表达式指代一个确定的对象"。xvalue 和 lvalue 同属 glvalue,在 identity 这一点上没有本质区别。
关键要分清:值类别是表达式的属性,不是对象的存储类别。
1. identity 的"内存"在哪里?
可以有两种理解:
- 如果问的是"identity 本身存在哪里":不需要为 identity 单独分配内存,它不是对象里隐藏的某个 ID 字段。
- 如果问的是"xvalue 指代的对象存在哪里" :由对象自身的存储期决定,与表达式是
lvalue还是xvalue无关。它可能具有自动、静态、线程或动态存储期(例如通过new创建);也可能是一个临时对象,例如std::move(std::string("tmp"))中,std::string("tmp")这个 prvalue 经过临时量实质化(C++17 起)成为临时对象,std::move返回的xvalue指代的就是它。
先看指代已有对象的例子:
cpp
#include <cassert>
#include <string>
#include <utility>
std::string s = "hello";
std::string&& r = std::move(s);
assert(&r == &s); // 指代同一个 string 对象
这里:
| 表达式 | 值类别 | 指代的对象 |
|---|---|---|
s |
lvalue |
对象 s |
std::move(s) |
xvalue |
仍然是对象 s |
r |
lvalue |
仍然是对象 s |
注意:虽然 r 的声明类型是右值引用,但使用名字 r 这个表达式是 lvalue。
std::move(s) 没有创建新对象,没有分配内存,也没有移动任何资源,它只是一个类型转换,让同一个对象以 xvalue 表达式的形式被使用。
2. lvalue 和 xvalue 的区别是什么?
不是"身份不同",而是"如何对待这个对象"不同。
lvalue:通常按"这个对象还要继续使用"的方式处理,不会自动把它当作可消耗的对象。xvalue:表示该对象的资源可以被重用,通过右值引用绑定、重载决议等机制,可能会选择移动操作。
例如:
cpp
std::string a = s; // s 是 lvalue,调用拷贝构造
std::string b = std::move(s); // 表达式是 xvalue,调用移动构造
但 xvalue 不保证一定发生移动,还取决于类型、const 限定以及可用的构造或赋值函数。
另外,xvalue 也不意味着对象马上销毁。上面的 std::move(s) 不会缩短 s 的生命周期。
3. 移动之后,原对象的 identity 去哪里了?
没有去任何地方,原对象仍然是原对象。
执行:
cpp
std::string b = std::move(s);
之后:
s仍然存在,地址和身份没有改变。b是另一个对象,有自己的身份。- 移动构造可能把
s管理的资源(例如堆上的字符缓冲区)转交给b,而不是把s对象本身从原地址搬走。 - 对
std::string而言,移动后的s处于有效但未指定的状态,不能假定其内容一定是什么。
提示:主流标准库实现通常有短字符串优化(SSO),
"hello"这样的短字符串直接存放在对象内部,没有堆缓冲区。此时移动的效果更接近拷贝,s也不一定被清空。这正是"有效但未指定"的体现,不要依赖移动后的具体内容。
也就是说,移动的是资源或状态,不是对象的 identity。
补充: 有 identity 不代表能直接写
&表达式。例如&std::move(s)不合法,因为内置取地址运算符要求操作数是lvalue;但可以像前面的例子一样,通过引用r观察同一个对象的地址。
一句话总结
identity 回答"这是哪一个对象";lvalue / xvalue 的区别回答"这个表达式允许我们怎样使用该对象"。
核心结论(identity 不占单独内存、lvalue 和 xvalue 都是 glvalue、&std::move(s) 不合法)都是对的。
修改记录:
- "对象原来在哪里,就在哪里"不完整。
xvalue不一定指代已有的具名对象,也可能指代临时对象(临时量实质化的结果)。补上了这种情况,并补全了存储期,漏了线程存储期。 - "会选择移动操作"说得太满。 它和后文"不保证一定发生移动"自相矛盾,改成"可能选择"。
- 移动资源的描述缺少前提。
"hello"这样的短字符串在主流实现里通常用 SSO(短字符串优化),没有堆缓冲区,移动时没有"转交堆上缓冲区"这回事。补了一句说明,并把示例措辞调整得更准确。