为什么 C++ 中构造表达式的结果是 prvalue?
这是 C++ 值类别(value category)设计的核心问题之一。先说明一点:构造函数本身并不"返回值",准确的说法是,构造表达式 T() 或 T(args...) 的值类别是 prvalue。
一、值类别回顾
C++11 之后,表达式主要分为三类:
| 值类别 | 含义 |
|---|---|
| lvalue(左值) | 有身份、可取地址的对象,不能被隐式移走 |
| xvalue(将亡值) | 有身份,但其资源可以被移走,例如 std::move(x) |
| prvalue(纯右值) | 没有身份,用来初始化对象或计算运算符的操作数 |
prvalue 不是对象本身,而是对象的初始化器,它描述"如何构造一个对象"。
二、为什么这样设计
1. 支持强制复制消除(C++17 起)
cpp
T x = T(1, 2);
// C++17 前:允许编译器消除临时对象,但不是强制的,
// 且拷贝/移动构造函数必须可访问
// C++17 起:保证直接在 x 的位置构造,零开销
prvalue 的语义是"描述如何初始化一个对象",而不是"已经存在的对象"。因此编译器可以直接在目标位置构造,不需要中间临时对象。
这也带来一个直接的好处:不可拷贝、不可移动的类型也能这样初始化。
cpp
struct Pinned {
Pinned() = default;
Pinned(const Pinned&) = delete;
Pinned(Pinned&&) = delete;
};
Pinned p = Pinned(); // C++17 起合法,C++14 及之前编译失败
2. 返回值优化的语言保证
cpp
T make() { return T(); }
T x = make(); // C++17 保证:直接构造 x,无拷贝无移动
如果 T() 产生的是 lvalue(一个已存在的对象),就必须先构造这个对象,再把它移动或拷贝到目标位置。prvalue 让"延迟物化"成为可能。
注意 :C++17 保证的是返回 prvalue 时的省略。对于具名局部变量的返回(NRVO),编译器通常会优化,但标准不保证:
cppT make2() { T t; return t; // NRVO:允许但不保证,T 仍需可移动 }
3. 引用绑定与生命周期
cpp
const T& r = T(); // prvalue 绑定到 const 引用,生命周期延长至 r 的作用域结束
T&& rr = T(); // 同理
prvalue 在绑定到引用时才会"物化"(temporary materialization)为临时对象,物化的结果是一个 xvalue,其生命周期被自动延长。
4. 语义更精确
cpp
T() // prvalue:描述"如何构造一个 T"
std::move(x) // xvalue:描述"x 可以被移走"
x // lvalue:描述"x 这个对象"
三者职责分明:
- prvalue:初始化来源
- xvalue:移动来源
- lvalue:持久对象
总结
prvalue 不是对象,而是对象的"配方"。 这样设计让编译器可以把"创建临时对象 → 拷贝/移动到目标"优化成"直接在目标位置构造"。从 C++17 起,这不再是编译器的可选优化,而是语言层面的保证。
修正日志:
- 标题和表述 :构造函数本身没有"值",准确说法是构造表达式
T(args...)的值类别是 prvalue。 - RVO 的范围 :C++17 保证的只是返回 prvalue 时的省略(如
return T();)。具名返回值优化(NRVO,如T t; return t;)并不保证,原文容易让人误解。 - 临时对象物化:补充了 prvalue 物化后得到的是 xvalue。
- 补充了不可拷贝/不可移动类型的例子,这是强制消除最直观的价值。