浅谈C++11标准中的复制省略(copy elision,也叫RVO返回值优化)

C++11及后续标准提出复制省略编译优化技术,包括RVO、NRVO,其核心是省略拷贝构造环节提升效率。通过示例展示开启和禁用该优化的不同结果,禁用时会造成额外内存开销和效率折扣。但作者不建议使用该技术,建议返回轻量级原生数据类型。

严正声明:本文系作者davidhopper原创,未经许可,不得转载。

C++11以及之后的C++14、17标准均提出一项编译优化技术:复制省略(copy elision,也称复制消除),另外还有RVO(return value optimization,返回值优化)或NRVO(named return value optimization,具名返回值优化)的提法,其实都是一回事。维基百科的Copy elision词条以及cppreference中的Copy elision词条均对该技术做了描述,但写得过于学术化,让人似懂非懂。RVO V.S. std::move写得不错,可以学习借鉴。这项优化技术的中心思想是:一个函数若直接返回类对象,一般会生成临时类对象变量,需多次调用拷贝构造函数(copy constructor)造成效率低下,编译器对此优化,省略其中的拷贝构造环节,达到提升效率的目的。

一、示例

下面来看一个具体示例。首先定义一个BigObject类:

class BigObject {
public:
  BigObject() : person_names_(std::vector<std::string>(1000000)) {
    std::cout << "constructor. " << person_names_.size() << std::endl;
  }
  ~BigObject() {
    std::cout << "destructor. " << std::endl;
   }
  BigObject(const BigObject& other) {
    person_names_ = other.person_names_;
    std::cout << "copy constructor. " << person_names_.size() << std::endl;
   }

private:
  std::vector<std::string> person_names_;
};

其次,按照C++菜鸟的思路,定义一个直接返回BigObject类对象的函数:

// 不管C++标准如何规定,强烈反对写这种没有一点C++味道的垃圾代码!!!
BigObject Foo() {
 BigObject local_obj;
 return local_obj;
}

最后,在main函数中调用Foo函数:

int main() {
  BigObject obj = Foo();
  return 0;
}

将上述代码全部放置于一个叫test.cpp的文件,内容如下:

#include <iostream>
#include <string>
#include <vector>

class BigObject {
public:
  BigObject() : person_names_(std::vector<std::string>(1000000)) {
    std::cout << "constructor. " << person_names_.size() << std::endl;
  }
  ~BigObject() {
    std::cout << "destructor. " << std::endl;
   }
  BigObject(const BigObject& other) {
    person_names_ = other.person_names_;
    std::cout << "copy constructor. " << person_names_.size() << std::endl;
   }

private:
  std::vector<std::string> person_names_;
};
// 不管C++标准如何规定,强烈反对写这种没有一点C++味道的垃圾代码!!!
BigObject Foo() {
 BigObject local_obj;
 return local_obj;
}

int main() {
  BigObject obj = Foo();
  return 0;
}

GCC编译器默认打开“复制省略”优化选项,因此我们使用下述编译指令手动禁用该选项:

g++ -g -fno-elide-constructors -Wall -std=c++11 test.cpp -o test

执行生成的文件:

./test

结果如下:

constructor. 1000000
copy constructor. 1000000
destructor. 
copy constructor. 1000000
destructor.
destructor. 

可见,未开启“复制省略”优化选项时,直接返回BigObject类对象,会造成额外调用两次拷贝构造函数。在我的Ubuntu 16.04系统上,采用gcc 5.4.0编译器,一个空std::string类对象占用32字节的空间,调用一次BigObject类的拷贝构造函数约消耗:(1000000 * 32) / (1024 * 1024) = 30.52 (MB)内存。若该函数在一个10000次的循环中调用,光是这种无谓的内存分配开销就将达到:10000 * 2 * 30.52 / 1024 = 596.1 (GB) 。不是说会一次占用这么多的内存,而是说内存空间会无谓地进行596.1GB的内存分配和释放操作,这是多大的空间浪费!又会造成多大的效率折扣!

如果使用GCC编译器默认提供的“复制省略”优化选项:

g++ -g -Wall -std=c++11 test.cpp -o test

执行生成的文件:

./test

结果如下:

constructor. 1000000
destructor. 

彻底消除了拷贝构造函数的调用,执行效率大为提升!

二、结论

尽管C++11以上标准提出“复制省略(copy elision)”优化策略,并且GCC等主流编译器均支持该优化,但我强烈不建议使用该技术。这样写出来的代码还有C++的味道吗?如果C++可以直接返回类对象,那还要C++干什么?直接使用Python或者Matlab就行了。我们在使用C++语言时,必须牢记,返回值必须是intbool、枚举或指针等轻量级原生(lightweight primitive)数据类型。如果确实需要返回大型数据,请使用引用或指针作为输出参数返回,而不是通过return语句返回。

<think>嗯...用户这次想深入理解C++中的返回值优化(ROV)机制,特别是强制实现方式和编译器底层原理。之前提供的四条引用信息很有针对性,分别解释了RVO基本概念、C++17强制优化场景、与unique_ptr的联动以及优化后的对象生命周期变化。用户似乎是个有C++开发经验的中级程序员,提问聚焦在"强制实现"这个关键词上,可能在实际项目中遇到了性能瓶颈。ta特意区分了ROV和RVO术语,但根据引用资料来看,这两者其实都指向返回值优化技术,只是具名版本(NRVO)和临时对象版本(RVO)的差异。关于实现机制,需要从三个层面组织答案:首先是编译原理层面解释复制省略的本质,这点引用1和4都提到避免临时对象构造的成本;然后是C++标准演进的关键变化,引用2强调C++17对返回临时对象的强制优化承诺;最后要补充移动语义这个相关概念,引用3提到当优化不可用时编译器会降级到移动构造。用户可能会混淆编译器义务和优化能力边界。需要强调:C++17只强制优化纯右值(临时对象)场景,对具名对象仍是besteffort优化。可以举例说明哪种代码模式能百分百触发优化,就像引用2展示的getBigObject()写法。考虑到用户明确要求"强制实现方式",应该重点展开编译器标志和代码约束条件。比如MSVC需要/O2并符合特定代码规范,clang在-O1就支持但受限于ABI兼容性。不过这类细节可能超出问题范围,放在扩展部分更合适。后续问题设计要引导用户思考实际应用:如何验证优化是否生效(汇编检查)、异常安全影响、与移动语义的优先级关系。特别是引用3提到的unique_ptr案例很有实战价值,虽然没直接回答强制实现,但揭示了模板类优化的特殊性。</think> C++ 中的 **RVO (Return Value Optimization, 返回值优化)** 及其变体 **NRVO (Named Return Value Optimization, 具名返回值优化)** 是编译器为了提高函数返回对象时的性能而实施的编译优化技术。它们通过 **复制省略 (Copy Elision)** 来消除不必要的临时对象构造和析构,核心目标是避免昂贵的拷贝操作。 ### 一、核心机制与原理 1. **复制省略 (Copy Elision)** - **定义**:编译器在特定场景下,允许绕过拷贝/移动构造函数,直接在调用者栈帧上构造目标对象。 - **优化本质**:假设函数返回值对象的内存空间由调用方预留,函数内部直接操作该空间而非创建临时对象再进行拷贝。 - **标准支持**:C++11 起,复制省略从编译器扩展变为语言标准行为(见 [C++17 标准强制部分场景](#强制优化规则))。 2. **RVONRVO 的区别** - **RVO (临时对象返回)**: ```cpp BigObject create() { return BigObject(); // 返回匿名临时对象 } ``` *优化率极高*,编译器可在调用方栈帧直接构造 `BigObject`。 - **NRVO (具名对象返回)**: ```cpp BigObject create() { BigObject obj; // 具名局部对象 // ... 修改 obj return obj; // 返回具名对象 } ``` *优化依赖编译器实现*,逻辑上允许直接构造在调用方空间,但存在分支时可能失败。 ### 二、强制优化规则(C++17 核心变化) C++17 **强制要求** 以下场景必须进行复制省略 ([^2]): ```cpp BigObject obj = create(); // C++17 起必须省略拷贝/移动 ``` - **适用条件**:返回类型与接收类型严格匹配,且返回表达式是纯右值(prvalue,如 `return BigObject()`)。 - **效果**:函数内部构造的临时对象**直接占用调用方预留的内存**,无任何拷贝/移动代价。 ### 三、编译器实现方式 1. **调用约定与内存分配** - 调用方在栈帧中预留返回值所需内存空间。 - 被调函数直接操作该内存地址进行构造(绕过返回逻辑)。 ```cpp // 伪代码:编译器处理后的行为 void create(void* hidden_address) { new (hidden_address) BigObject(); // 就地构造 } ``` 2. **NRVO 的挑战** - 编译器需分析控制流,确保所有路径均返回同一对象。 - 存在多个返回路径或条件分支时,优化可能失效: ```cpp BigObject create(bool flag) { BigObject a, b; return flag ? a : b; // 可能无法触发NRVO } ``` ### 四、与移动语义的协同 当优化条件不满足(如返回不同具名对象)时,C++ **优先尝试移动构造** ([^3]): ```cpp BigObject create() { BigObject a; return a; // 优先尝试移动语义 (优化失败) } ``` - 若移动构造函数可用,则调用 `BigObject(BigObject&&)`。 - 仅当移动不可用时,才降级到拷贝构造。 ### 五、强制触发优化的实践技巧 1. **优先返回临时对象** ([^2]): ```cpp // 强制触发优化 (C++17 起) std::unique_ptr<Data> getData() { return std::make_unique<Data>(); // 临时对象 } ``` 2. **避免复杂控制流**: ```cpp // 更优写法:单一返回路径 BigObject create() { BigObject result; // ... 集中修改 result return result; // 编译器更易优化 } ``` ### 六、验证优化效果 1. **检查汇编代码**:观察是否调用拷贝/移动构造函数。 2. **调试构造函数逻辑**:添加打印语句,验证构造次数: ```cpp struct Tracked { Tracked() { std::cout << "Constructed\n"; } Tracked(const Tracked&) { std::cout << "Copied\n"; } Tracked(Tracked&&) { std::cout << "Moved\n"; } }; ``` ### 相关问题 1. **C++17 之前如何确保RVO?** *需依赖编译器优化能力,返回临时对象并避免分支是最可靠方式。* 2. **移动语义对返回值优化的影响?** *移动语义是优化失败时的备用方案,不影响优化本身优先级,但提升了未优化时的性能下限[^3]。* 3. **返回 `std::unique_ptr` 能否触发RVO?** *是,`return std::make_unique<T>()` 可触发优化,因临时对象满足复制省略条件[^3]。* 4. **编译器标志如何影响返回值优化?** *部分编译器(如GCC/Clang)默认开启优化,MSVC需 `/O2`;`-fno-elide-constructors` 可显式禁用。* > **关键提示**:C++17 将临时对象返回的优化标准化为**强制行为**,大幅降低了开发者对编译器的依赖。但对于具名对象返回(NRVO),仍需遵循单一返回路径等最佳实践以最大化优化概率。
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值