C++ 拷贝省略与 RVO
更新时间:2026-08-27。本文是
languages/cpp/主题高手层第 13 篇。上一篇移动语义说"大对象传值要小心拷贝",这篇讲一个好消息:很多拷贝编译器根本不生成。写return make_big();不会真的拷贝——编译器直接让返回值"就地构造"。理解拷贝省略,才能正确评估代码的真实拷贝成本,也能解释一些"奇怪"的编译现象。
本文要回答的问题
- 拷贝省略(copy elision)是什么?编译器什么时候可以省掉拷贝?
- RVO 和 NRVO 有什么区别?C++17 的"保证性省略"是什么?
-fno-elide-constructors关闭省略后会发生什么?- 拷贝省略和移动语义是什么关系?返回局部对象到底拷几次?
直观感受:返回大对象竟然零拷贝
#include <vector>
std::vector<int> make_big() {
std::vector<int> v(100000, 1);
return v; // 返回局部对象
}
int main() {
auto data = make_big(); // 你以为拷贝了一次?
}直觉上:make_big 里构造 v → 返回时拷贝/移动给调用者 → 拷贝 10 万个 int。但实际上一次拷贝都没有。编译器执行返回值优化(RVO,Return Value Optimization):直接在调用者的 data 位置上构造 v,函数里的 v 和外面的 data 是同一个对象。
// 编译器的做法(示意):调用者分配好位置,直接往里构造
auto data = make_big();
// make_big 内部的 v 就地构造在 data 的位置上,返回时什么都不用做RVO 与 NRVO
| 名称 | 全称 | 触发条件 | 示例 |
|---|---|---|---|
| RVO | Return Value Optimization | 返回临时对象 | return make_big(); |
| NRVO | Named RVO | 返回具名局部对象 | return v;(v 是局部变量) |
| 参数省略 | 参数传递省略 | 传临时对象给值参数 | f(make_big()) |
2.1 RVO:返回临时对象
std::vector<int> make() {
return std::vector<int>(100000, 1); // 返回临时对象
}2.2 NRVO:返回具名局部变量
std::vector<int> make() {
std::vector<int> v(100000, 1);
return v; // 返回具名局部对象(NRVO)
}注意:RVO 在 C++17 是保证的(编译器必须做),NRVO 是允许但非强制的(编译器"尽力而为")。多数主流编译器都做 NRVO,但不是标准保证。
2.3 C++17 保证性省略
C++17 前,RVO 只是"优化"——允许但可能不做。C++17 起,返回临时对象的省略是标准保证的(guaranteed copy elision),编译器没有选择。这意味着:
struct OnlyMovable {
OnlyMovable() {}
OnlyMovable(OnlyMovable&&) = default; // 只有移动
OnlyMovable(const OnlyMovable&) = delete; // 删除拷贝
};
OnlyMovable make() {
return OnlyMovable{}; // C++17:保证省略,不需要可拷贝/可移动!
}在 C++17 下这段代码合法——因为省略是保证的,连移动构造都不需要。这正是 std::vector<bool> 特化、工厂函数等场景能返回不可拷贝对象的原因。

关闭省略:看看真实成本
GCC/Clang 提供 -fno-elide-constructors 关闭省略,可以观察"没有优化时"的拷贝次数(教学/调试用):
#include <iostream>
struct Big {
Big() { std::cout << "ctor\n"; }
Big(const Big&) { std::cout << "copy\n"; }
Big(Big&&) { std::cout << "move\n"; }
};
Big make() {
Big b;
return b; // NRVO
}
int main() {
auto x = make();
}| 编译选项 | 输出 | 说明 |
|---|---|---|
| 默认(-O2) | ctor | NRVO 生效,零拷贝零移动 |
-fno-elide-constructors -O2 | ctor + move | 省略被关,退化为移动 |
| 早期 C++11 无优化 | ctor + copy + move | 最坏情况 |
结论:现代编译器默认会把常见返回场景优化到零拷贝,移动语义反而是"编译器没省掉时"的兜底。
拷贝省略 vs 移动语义
| 对比项 | 拷贝省略 | 移动语义 |
|---|---|---|
| 本质 | 编译器直接不生成拷贝 | 用"偷资源"替代"深拷贝" |
| 时机 | 编译期 | 运行时 |
| 依赖 | 对象就地构造 | 需要移动构造/移动赋值 |
| 优先级 | 编译器优先省略 | 省略不了时用移动 |
| 目的 | 消灭拷贝 | 减轻拷贝 |
两者是"优化顺序"关系:编译器先尝试省略(连移动都省了),省略不了才退化为移动,再不行才拷贝。 这也解释了为什么 std::move 在返回局部对象时不需要也不应该写:
std::vector<int> make() {
std::vector<int> v;
return std::move(v); // 反优化!阻止 NRVO
}return std::move(v) 会抑制 NRVO——因为返回的不再是"具名局部对象 v"而是"v 的右值引用",编译器无法省略,只能退化为移动。返回局部对象直接 return v; 即可,编译器会做得更好。
拷贝省略的实际收益
| 场景 | 无省略时 | 有省略时 |
|---|---|---|
| 返回大 vector | 深拷贝 10 万元素 | 零拷贝 |
| 工厂函数返回 unique_ptr | 移动一次 | 零拷贝(C++17 保证) |
| 链式构造 | 多层拷贝 | 零拷贝 |
| 传临时对象给值参数 | 拷贝 | 零拷贝 |
工厂函数场景特别重要:make_unique / make_shared 返回对象、emplace_back 就地构造,都在享受拷贝省略红利。
与本站性能主线衔接
- 移动语义延伸:省略不了时靠移动兜底,见 移动语义与右值引用。
- 编译器优化:RVO 属于 编译器优化 的返回优化家族,
-O2下更激进。 - 性能剖析:若火焰图中出现大规模
memcpy,可能是省略没生效或返回值没接好,用 perf 验证。 - 智能指针:工厂函数返回 unique_ptr 的零拷贝依赖省略,见 make_unique。
一句话总结
拷贝省略让"返回/传值"经常零拷贝:C++17 保证临时对象返回的省略(RVO),NRVO 对具名局部变量尽力而为,移动语义是省略失败后的兜底;返回局部对象直接 return v;,别写 return std::move(v) 自断优化。
上一篇:STL 容器选型深挖 下一篇:SFINAE