【C++】关于cpp的RVO/NRVO

RVO(Return Value Optimization,返回值优化)。NRVO(Named RVO,具名返回值优化)。
返回值直接构造:返回大对象时,编译器优化,让返回值直接在调用处构造,减少一次拷贝开销
C++17起,由于RVO,不需要在返回大对象时考虑将返回参数作为入参放在形参列表,这样代码可读性降低

  • 符合RVO的条件:返回值和要返回的局部对象类型相同
A a;//a是一个big object
void func(A& a){...}
func(a);

//而是可以语义清晰的直接返回
A a = func();

一、为什么会有 RVO?——先看清“不优化”时有多浪费

假设我们有一个“很重”的对象 BigObject(拷贝起来很费劲):

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

再来一个工厂函数:

BigObject makeObj() {
    BigObject local;   // 步骤1:在 makeObj 栈帧里构造
    return local;      // 步骤2:理论上要拷贝给调用者
}

如果没有优化,流程是:

  1. makeObj 里先给 local 分配一次内存,并构造一次。
  2. 返回时,再拷贝一份给调用者。
  3. 离开作用域时,local 析构一次。

也就是说:
“构造 1 次 + 拷贝 1 次 + 析构 1 次” —— 拷贝是白花花的开销。


二、RVO 做了什么?——把两次构造合并成一次

打开优化后(GCC/Clang 默认就打开),编译器会偷偷改写成下面这样:

// 伪代码:编译器视角
void makeObj(void* ret_addr) {   // ret_addr 是调用者预留好的“目的地”
    new (ret_addr) BigObject;    // 直接在目的地构造
}

于是流程变成:
“构造 1 次 + 0 次拷贝 + 析构 1 次”
省掉了中间的拷贝/移动,这就是 RVO


三、两个常见变体

  1. 无名 RVO(最经典)
    返回一个“纯右值”临时对象:

    BigObject makeObj() {
        return BigObject{};   // 直接返回临时对象
    }
    
  2. 命名 RVO(NRVO)
    返回一个“具名局部变量”:

    BigObject makeObj() {
        BigObject obj;        // 具名
        return obj;           // NRVO 也可能触发
    }
    

C++17 之前:两种都属于“允许优化”,编译器可以选做不做。
C++17 之后:第 1 种(返回纯右值)变成“强制优化”,标准规定必须省。


四、动手验证:把优化关掉/打开看输出

代码(gcc/clang 通用):

#include <iostream>
struct A {
    A()  { std::cout << "Ctor\n"; }
    A(const A&) { std::cout << "Copy\n"; }
};
A get() { return A{}; }   // 无名 RVO
int main() {
    A a = get();
}
  • 默认编译(打开优化)

    g++ test.cpp -std=c++11
    ./a.out
    

    输出:

    Ctor
    

    只有一次构造,拷贝被省略。

  • 关闭优化

    g++ test.cpp -std=c++11 -fno-elide-constructors
    ./a.out
    

    输出:

    Ctor
    Copy
    

    这回拷贝出现了。


五、C++17 的“强制 RVO”

从 C++17 开始,只要返回的是纯右值,即使拷贝/移动构造函数有副作用,也必须省略
示例:

struct NoCopy {
    NoCopy() = default;
    NoCopy(const NoCopy&) = delete;   // 删了拷贝
    NoCopy(NoCopy&&)      = delete;   // 删了移动
};

NoCopy foo() {
    return NoCopy{};   // C++17 合法,直接构造在目标位置
}

在 C++14 里,这段代码会因为删除拷贝/移动而编译失败;C++17 则 OK。


六、小结记忆

  • RVO 的目标:把“构造+拷贝+析构”三件套,压缩成“构造+析构”两件套
  • 无名 RVO(返回临时对象)最容易触发;命名 RVO(返回局部变量)依赖编译器。
  • 想自己验证:用 -fno-elide-constructors 开关即可。
  • C++17 起,纯右值返回场景下一定发生 RVO,标准强制。

一句话总结:
RVO 就是“编译器帮你在正确的内存位置上一次性把事情做完”,从而省掉来回搬运对象的成本。

posted @ 2025-08-20 15:02  仰望星河Leon  阅读(75)  评论(0)    收藏  举报