22. 复制省略与返回值优化(RVO/NRVO)

什么是复制省略?

复制省略,说白了就是编译器偷偷把你的拷贝动作给砍掉了。

你写代码时可能觉得:「我明明传值返回,肯定要调拷贝构造啊。」但实际运行时,编译器发现这个拷贝是多余的,就直接跳过了。嗯,这就是复制省略。

我刚开始学C++时,看到这个特性还以为是编译器出bug了。后来才明白,这是标准允许的优化手段。

复制省略主要发生在两种场景:

  • 返回值优化(RVO):函数返回临时对象时,直接构造到调用方的接收变量中
  • 具名返回值优化(NRVO):函数返回一个具名的局部对象时,同样省略拷贝

核心要点:复制省略不是编译器偷懒,而是C++标准明确允许的优化。从C++17开始,某些场景下的复制省略甚至是强制的。

编译器如何优化?

我们来看一个最典型的例子:

#include <iostream>

struct BigObject {
    BigObject() { std::cout << "构造\n"; }
    BigObject(const BigObject&) { std::cout << "拷贝构造\n"; }
    ~BigObject() { std::cout << "析构\n"; }
};

BigObject createObject() {
    BigObject obj;
    return obj;  // 这里本该拷贝一次
}

int main() {
    BigObject result = createObject();
    return 0;
}

你猜输出几行?

如果没有优化,应该是:构造 → 拷贝构造 → 析构(临时对象) → 析构(result)。但实际运行,很多编译器只输出:构造 → 析构。

为什么会这样?

编译器在生成代码时,直接把 result 的地址传给了 createObject 内部。局部对象 obj 直接在 result 的位置上构造。返回时根本不需要拷贝。

我个人习惯在调试这类问题时,把拷贝构造和移动构造都加上打印。这样能清楚看到编译器到底走了哪条路。

RVO vs NRVO

优化类型 场景 编译器支持度 C++17 强制?
RVO 返回临时对象(如 return BigObject() 几乎所有编译器都支持
NRVO 返回具名局部对象(如 return obj; 大多数编译器支持,但非强制

RVO 和 NRVO 的区别其实就一句话:RVO 返回的是「无名氏」,NRVO 返回的是「有名字的变量」。

我在项目中遇到过一个问题:某个函数返回一个 std::string,代码里写的是 return std::move(str)。结果 NRVO 反而被抑制了,因为 std::move 返回的是右值引用,编译器认为你「主动要求移动」,就不再尝试省略拷贝了。

避坑指南:不要为了「优化」而手动写 return std::move(local_var)。这反而可能阻止 NRVO。让编译器自己决定就好。

强制复制省略(C++17)

C++17 引入了一个重要变化:在某些场景下,复制省略不再是「可以优化」,而是「必须优化」。

具体来说,当返回一个纯右值(prvalue)时,编译器必须省略拷贝。比如:

BigObject createObject() {
    return BigObject();  // C++17 强制省略拷贝
}

BigObject obj = createObject();  // 直接构造,没有拷贝

在 C++17 之前,即使编译器做了优化,拷贝构造函数也必须是可访问的(不能是 private 或 delete)。但从 C++17 开始,如果发生了强制复制省略,拷贝构造函数甚至不需要存在。

你想想看,这意味着什么?

意味着你可以写出这样的代码:

struct NonCopyable {
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;  // 禁止拷贝
};

NonCopyable create() {
    return NonCopyable{};  // C++17 下合法!
}

int main() {
    auto obj = create();  // 没问题,没有拷贝
}

这在 C++14 及之前是编译不过的。我记得当时有个同事从 C++17 切回来,写了这样的代码,CI 直接报错,排查了半天才发现是拷贝构造函数被删除了。

小技巧:如果你想让某个对象「只能移动不能拷贝」,但又想支持 RVO,C++17 的强制复制省略可以帮你省掉很多麻烦。不过要注意,NRVO 仍然不是强制的,所以返回具名局部对象时,拷贝构造函数还是得保留。

知识体系图

下面这张图帮你理清复制省略的完整脉络:

复制省略(Copy Elision)知识体系 复制省略 RVO(返回值优化) NRVO(具名返回值优化) 返回临时对象 C++17强制 拷贝构造可删除 返回具名变量 非强制 避免move 核心原则:让编译器自己优化,不要画蛇添足 ⚠ 避坑:return std::move(local) 会抑制 NRVO

总结

复制省略是C++中一个「看不见但很重要」的优化。它让代码跑得更快,而且不需要你写任何特殊代码。

我个人建议:

  • 写返回语句时,直接 return 对象,别加 std::move
  • 如果编译器支持 C++17,放心使用强制复制省略的特性
  • 调试时如果怀疑拷贝次数,加打印日志验证一下

我曾经在一个性能敏感的项目里,靠 NRVO 把某个热点函数的拷贝次数从 3 次降到了 0 次。改动就一行代码——删掉了多余的 std::move。嗯,有时候优化就是这么简单。