将亡值(xvalue)是C++移动语义的核心,表示有身份且可移动的临时对象。其关键特性是:虽有名称(可取地址),但资源可转移。典型场景包括
std::move、右值引用返回及类型转换。然而,变量作为表达式始终为左值 ,即使类型为右值引用,导致模板转发中若未使用std::forward,会错误绑定左值,引发拷贝而非移动。此外,返回局部变量的右值引用会导致悬垂引用,而const T&&会阻止移动。工程中应遵循:变量即左值,转发用forward,转移用move,避免对局部变量std::move。
1. 值类别体系(C++11 起)
C++11 将表达式的值类别分为三类:
表达式
├── 泛左值 (glvalue)
│ ├── 左值 (lvalue)
│ └── 将亡值 (xvalue, eXpiring value)
└── 右值 (rvalue)
├── 将亡值 (xvalue)
└── 纯右值 (prvalue, pure rvalue)
核心区分标准:
| 类别 | 是否有身份(identity) | 是否可移动 |
|---|---|---|
| 左值 | 有 | 通常不应移动 |
| 将亡值 | 有 | 可移动 |
| 纯右值 | 无 | 可移动 |
将亡值是唯一同时具有"身份"和"可移动性"的值类别。
2. 什么是将亡值
将亡值表示一个对象的资源即将被转移,但该对象本身仍有名称或可被引用。
典型产生场景:
// 1. 返回右值引用的函数调用
std::vector<int>&& get_temp(); // 返回类型是右值引用 → 表达式是 xvalue
// 2. std::move 的结果
std::move(x); // 结果是 xvalue
// 3. 转换为右值引用的转换
static_cast<std::vector<int>&&>(v); // xvalue
// 4. 返回非引用类型的函数调用是 prvalue,不是 xvalue
// (C++17 的 guaranteed copy elision 改变了这一点)
std::vector<int> make_vec(); // prvalue,不是 xvalue
3. 关键区分:xvalue vs prvalue
这是理解参数传递问题的核心:
std::vector<int> v;
// xvalue:有身份,可移动
std::move(v) // 有身份(就是 v),可移动
// prvalue:无身份,可移动
std::vector<int>{1,2,3} // 临时对象,无身份
| 场景 | 表达式类别 | 能否取地址 | 能否绑定到左值引用 |
|---|---|---|---|
std::move(x) |
xvalue | 能(&x合法) |
不能 |
T{} |
prvalue | 不能 | 不能 |
return T{} |
prvalue | 不能 | 不能 |
4. 参数传递中的 xvalue 陷阱
4.1 经典问题:std::move 后继续使用
void process(std::string s);
std::string str = "hello";
process(std::move(str)); // str 的资源已转移
std::cout << str; // str 处于有效但未指定状态(通常为空)
std::move(str)是 xvalue,它告诉编译器"可以移动 str 的资源"。移动后 str处于有效但未指定状态,继续使用的语义取决于类型。
4.2 更隐蔽的问题:xvalue 作为函数参数时的重载决议
void foo(const std::string& s); // #1 接受左值
void foo(std::string&& s); // #2 接受右值
std::string s = "test";
foo(s); // 调用 #1,s 是左值
foo(std::move(s)); // 调用 #2,std::move(s) 是 xvalue
foo(std::string("temp")); // 调用 #2,prvalue
问题出在转发场景:
template<typename T>
void wrapper(T&& arg) { // 万能引用
foo(arg); // 问题在这里
}
std::string s = "test";
wrapper(s); // T = std::string&, arg 是左值引用 → 调用 #1
wrapper(std::move(s)); // T = std::string, arg 是右值引用 → arg 本身是左值!
// 调用 #1,不是 #2!
关键洞察:T&&在模板中是万能引用,但当 T被推导为非引用类型时,arg本身是左值(有名称的变量),即使它绑定到的是 xvalue。
4.3 为什么 xvalue 表达式在参数传递中会"退化"
void foo(std::string&& s);
template<typename T>
void wrapper(T&& arg) {
// arg 的类型是 T&&,但 arg 作为表达式,是左值
// 因为:有名称的变量/参数,即使是右值引用类型,作为表达式也是左值
foo(arg); // 编译错误:不能将左值绑定到右值引用
}
这是 C++ 最反直觉的规则之一:
| 声明 | 作为表达式时的类别 |
|---|---|
T&& x(x 是变量名) |
左值(因为 x 有名称) |
std::move(x) |
xvalue |
static_cast<T&&>(x) |
xvalue |
返回 T&&的函数调用 |
xvalue |
变量是左值,即使它的类型是右值引用。
4.4 正确转发:std::forward
template<typename T>
void wrapper(T&& arg) {
foo(std::forward<T>(arg)); // 正确转发值类别
}
std::forward<T>(arg)的行为:
-
如果
T是左值引用类型 → 返回左值 -
如果
T是非引用类型 → 返回 xvaluewrapper(s); // T = std::string& → forward 返回左值 → 调用 #1
wrapper(std::move(s)); // T = std::string → forward 返回 xvalue → 调用 #2
5. xvalue 表达式导致参数传递失败的典型场景
5.1 忘记 forward 导致性能问题
class Processor {
public:
void add(std::vector<int>&& v) {
data_ = std::move(v); // 移动赋值
}
void add(const std::vector<int>& v) {
data_ = v; // 拷贝赋值
}
};
template<typename T>
void dispatch(T&& vec) {
Processor p;
p.add(vec); // vec 是左值,调用拷贝版本
}
std::vector<int> v = {1,2,3};
dispatch(std::move(v)); // 期望移动,实际拷贝
修复:
p.add(std::forward<T>(vec)); // 正确
5.2 返回 xvalue 导致悬垂引用
std::string&& bad_return() {
std::string s = "temp";
return std::move(s); // 返回局部变量的右值引用
} // s 销毁,返回悬垂引用
std::move(s)是 xvalue,但它引用的对象 s在函数结束时销毁。返回类型 std::string&&延长不了生命周期。
正确做法:
std::string good_return() {
std::string s = "temp";
return s; // NRVO 或移动构造
}
5.3 xvalue 与 const 右值引用的交互
void foo(const std::string&& s); // 极少见的签名
std::string s = "test";
foo(std::move(s)); // xvalue 绑定到 const 右值引用
// 但 const 右值引用不能移动
void bar(const std::string&& s) {
std::string t = std::move(s); // 调用拷贝构造函数!
// 因为 s 是 const,不能移动
}
const 右值引用会阻止移动语义。
6. 表达式类别判定规则总结
| 表达式形式 | 类别 | 原因 |
|---|---|---|
| 变量名(非函数) | 左值 | 有名称 |
| 函数名 | 左值 | 有名称 |
++x |
左值 | 返回引用 |
x++ |
prvalue | 返回临时值 |
&x |
prvalue | 返回指针(临时) |
std::move(x) |
xvalue | 显式转换 |
static_cast<T&&>(x) |
xvalue | 显式转换 |
返回 T&&的函数调用 |
xvalue | 返回类型是右值引用 |
x.y(y 是成员) |
与 x 同类别 | 继承 x 的类别 |
xp->y |
左值 | 成员访问 |
a[n] |
左值 | 数组元素 |
| 字面量 | prvalue | 无身份 |
7. 工程实践指南
7.1 接口设计原则
| 场景 | 签名 | 原因 |
|---|---|---|
| 只读访问 | const T& |
接受所有类别 |
| 获取副本 | T(按值) |
依赖移动语义 |
| 转移资源 | T&& |
只接受 xvalue/prvalue |
| 转发 | T&&+ std::forward |
保持原始值类别 |
7.2 避免的写法
// 返回右值引用
T&& foo();
// const 右值引用参数
void bar(const T&&);
// 在返回语句中用 std::move
T foo() {
T local;
return std::move(local); // 阻止 NRVO
}
// 正确
T foo() {
T local;
return local; // 允许 NRVO 或隐式移动
}
7.3 何时使用 std::move
| 场景 | 是否使用 |
|---|---|
| 返回局部变量 | 不用,依赖隐式移动 |
| 函数参数内部 | 转移参数资源 |
| 成员变量赋值 | 避免拷贝 |
| 转发参数 | 用 std::forward |
| 循环中的临时 | 明确转移意图 |
8. 总结
将亡值(xvalue)是 C++ 移动语义的核心机制,但它引入了一个关键陷阱:
有名称的变量,即使类型是右值引用,作为表达式也是左值。
这导致:
-
模板转发中忘记
std::forward→ 移动语义失效 -
返回右值引用 → 悬垂引用
-
const 右值引用 → 阻止移动
理解 xvalue 的本质:它是一个"即将被移动的有身份对象" ,但一旦它被绑定到一个有名称的变量上,它就"降级"为左值,需要显式 std::move或 std::forward才能重新获得移动能力。
三条铁律:
-
变量是左值,不管类型是什么
-
转发用
std::forward,转移用std::move -
返回局部变量不要
std::move,让它自然移动或 NRVO