C++核心机制-复制消除

这一块的内容主要设计:返回值优化,以及参数传递时候的优化。

避免写成return std::move(Myclass())的低质量代码。

0、测试代码

c 复制代码
#include <iostream>
#include <string>
#include <vector>

using namespace std;
class LoudBot {
public:
    std::string _name;

    // 1. 普通构造
    LoudBot(std::string name) : _name(name) {
        std::cout << "  [Construct] " << _name << " @" << this << std::endl;
    }

    // 2. 拷贝构造
    LoudBot(const LoudBot& other) : _name(other._name) {
        std::cout << "  [Copy] from " << other._name << " @" << this << std::endl;
    }

    // 3. 移动构造
    LoudBot(LoudBot&& other) noexcept : _name(std::move(other._name)) {
        std::cout << "  [Move] from " << _name << " @" << this << std::endl;
    }

    // 4. 析构
    ~LoudBot() {
        std::cout << "  [Destruct] " << _name << " @" << this << std::endl;
    }
};

// 场景 A: URVO (Unnamed RVO) - 返回无名临时对象
LoudBot getUnnamed() {
    return LoudBot("UnnamedBot"); 
}

// 场景 B: NRVO (Named RVO) - 返回具名局部变量
LoudBot getNamed() {
    LoudBot b("NamedBot");
    
    return b;
}

// 场景 C: 反面教材 - 手动 move 局部变量 (阻止了 RVO)
LoudBot getMoved() {
    LoudBot b("MovedBot");
    return std::move(b); // 千万别这么干!
}

template<typename T>
void func_for_x(T param){
    cout<<"func_for_x"<<endl;
}

template<typename T>
void func_for_cx(const T param){
    cout<<"func_for_cx"<<endl;
}

template<typename T>
void func_for_rx(const T& param){
    cout<<"func_for_rx"<<endl;
}

void test1(){
    //测试形参传递优化
    cout<<"---1---"<<endl;
    func_for_x(LoudBot("WF"));
    cout<<"---2---"<<endl;
    func_for_cx(LoudBot("WF"));
    cout<<"---3---"<<endl;
    func_for_rx(LoudBot("WF"));

    LoudBot val("WF");

    cout<<"---4---"<<endl;
    func_for_x(val);
    cout<<"---5---"<<endl;
    func_for_cx(val);
    cout<<"---6---"<<endl;
    func_for_rx(val);
}

void test2(){
    // 测试返回值优化
    std::cout << "--- 1. Testing URVO (getUnnamed) ---" << std::endl;
    LoudBot bot1 = getUnnamed();

    std::cout << "\n--- 2. Testing NRVO (getNamed) ---" << std::endl;
    LoudBot bot2 = getNamed();

    std::cout << "\n--- 3. Testing Pessimizing Move (getMoved) ---" << std::endl;
    LoudBot bot3 = getMoved();
    
    std::cout << "\n--- End of Test2 ---" << std::endl;
    return;
}

int main(){
    test1();
    test2();
    return 0;
}

开启禁止优化:-fno-elide-constructors 会发现:

sql 复制代码
---1---
  [Construct] WF @0x7ffee7238d00
  [Move] from WF @0x7ffee7238d20
func_for_x
  [Destruct] WF @0x7ffee7238d20
  [Destruct]  @0x7ffee7238d00
---2---
  [Construct] WF @0x7ffee7238d00
  [Move] from WF @0x7ffee7238d20
func_for_cx
  [Destruct] WF @0x7ffee7238d20
  [Destruct]  @0x7ffee7238d00
---3---
  [Construct] WF @0x7ffee7238d20
func_for_rx
  [Destruct] WF @0x7ffee7238d20
  [Construct] WF @0x7ffee7238ce0
---4---
  [Copy] from WF @0x7ffee7238d20
func_for_x
  [Destruct] WF @0x7ffee7238d20
---5---
  [Copy] from WF @0x7ffee7238d20
func_for_cx
  [Destruct] WF @0x7ffee7238d20
---6---
func_for_rx
  [Destruct] WF @0x7ffee7238ce0

默认编译:会启动优化

ruby 复制代码
xvxing@xvxing:~/study/algorithm/CF$ g++ -std=c++14 test.cpp -o c14_default
xvxing@xvxing:~/study/algorithm/CF$ ./c14_default 
---1---
  [Construct] WF @0x7ffdfa1b95e0
func_for_x
  [Destruct] WF @0x7ffdfa1b95e0
---2---
  [Construct] WF @0x7ffdfa1b95e0
func_for_cx
  [Destruct] WF @0x7ffdfa1b95e0
---3---
  [Construct] WF @0x7ffdfa1b95e0
func_for_rx
  [Destruct] WF @0x7ffdfa1b95e0
  [Construct] WF @0x7ffdfa1b95c0
---4---
  [Copy] from WF @0x7ffdfa1b95e0
func_for_x
  [Destruct] WF @0x7ffdfa1b95e0
---5---
  [Copy] from WF @0x7ffdfa1b95e0
func_for_cx
  [Destruct] WF @0x7ffdfa1b95e0
---6---
func_for_rx
  [Destruct] WF @0x7ffdfa1b95c0

下面介绍的是核心操作返回值优化,相比较上面的传递形参,会多一个知识点:返回临时对象槽


1、返回值优化

核心总结

术语 全称 含义 C++17 前状态 C++17 后状态
URVO Unnamed RVO 返回无名临时对象 编译器优化 (可被禁用) 语言特性 (强制执行,不可禁用)
NRVO Named RVO 返回具名局部变量 编译器优化 (可被禁用) 编译器优化 (依然可被禁用)
GCE Guaranteed Copy Elision 强制复制消除 不存在 标准强制规定 (针对纯右值)

知识点

1:参数传递中的临时对象

  • 当你调用 func(BigObj()) 时,BigObj() 这个表达式直接在 func 的形参栈内存地址上进行构造。

  • 这不涉及"先构造一个临时对象,再 Move 进形参"。

  • 例外:除非该参数是按值传递且 ABI(二进制接口)强制要求通过寄存器传递小对象,否则在逻辑上就是一次直接构造。

2:返回值的演变

  • 结论C++17 彻底消灭了 Prvalue(纯右值)返回值的"临时槽"。

  • C++14 及以前

    • return BigObj(); 逻辑上是:构造临时对象 -> (可能 Move/Copy)到返回值槽->析构临时对象 -> (可能 Move/Copy)到接收端变量 -> 析构返回对象槽中的内容。
    • 虽然编译器会进行优化(RVO),但这只是"优化"。如果使用了 -fno-elide-constructors,你会看到拷贝/移动构造函数的调用。
  • C++17 及以后

    • return BigObj(); 不再创建临时对象。BigObj() 被视为一个"初始化指令",直接在接收端的内存地址(Result Slot)上执行构造。
    • 关键点 :这不再是优化,而是语法规则。即使加上 -fno-elide-constructors,也不会产生拷贝或移动。

3:无名临时对象 (URVO)

  • 正解return BigObj(); 是最高效的(C++17 下 0 Copy, 0 Move)。

  • 反面教材return std::move(BigObj());return std::move(temp);

    • 原因std::move 强制将一个表达式转为 xvalue(将亡值)。
    • 后果 :这破坏了"直接构造"的上下文。编译器被迫认为你要"移动"一个东西,因此它必须先创建一个临时对象,然后调用 Move Constructor
    • 代价:多一次移动构造 + 多一次析构。

4:具名对象 (NRVO) 的特殊性

  • C++17 对于无名临时对象,即使手动禁止拷贝擦除,也会强制在实际空间构造
  • 对于 NRVO (LoudBot b; return b;),C++17 并没有 将其升级为强制标准。可以通过-fno-elide-constructors禁止优化。

场景行为解析 (配合代码)

场景 A: return LoudBot("Unnamed"); (URVO)

  • C++14 (默认) : 1次构造 (优化掉了拷贝)。
  • C++14 (关优化) : 1次构造 + 1次 Move + 1次 Destruct (临时对象)。
  • C++17 (无论开关) : 1次构造。 (直接在接收端构造)。

场景 B: LoudBot b; return b; (NRVO)

  • C++17 (默认) : 1次构造 (智能优化)。

  • C++17 (关优化) : 1次构造 + 1次 Move + 1次 Destruct (局部变量 b 死掉了)。

    • 注:此处证明 NRVO 不是强制的。

场景 C: LoudBot b; return std::move(b); (反面教材)

  • 行为 : 1次构造 + 1次 Move + 1次 Destruct。

  • 评价 : 显式 move 此时不仅多余,而且是有害的。因为它阻断了编译器进行 NRVO 的可能性(NRVO 要求返回的是变量本身,而不是变量的引用/转换结果)。


实战注意

  1. 永远不要对返回值使用 std::move

    1. return std::move(local_var); -> (破坏 NRVO)。
    2. return local_var; -> (编译器会自动尝试 NRVO,失败则自动隐式 Move)。
  2. 能用无名对象就用无名对象

    1. return LoudBot(args);LoudBot b(args); return b; 更可靠,因为前者在 C++17 下有标准层面的零拷贝保证。
  3. 参数传递

    1. void func(LoudBot b) + func(LoudBot("name")) 是非常高效的 Sink (接收) 模式,C++17 下零开销。
相关推荐
开心猴爷1 小时前
在 CICD 中实践 Fastlane + Appuploader 命令行,构建可复制的 iOS 自动化发布流程
后端
一 乐1 小时前
高校评教|基于SpringBoot+vue高校学生评教系统 (源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
疯狂的程序猴2 小时前
Web 抓包完整实践指南,从浏览器网络调试到底层数据流捕获的全流程方案
后端
喵手2 小时前
我使用openEuler构建出了一个自愈式系统监控平台
后端
调试人生的显微镜2 小时前
以 uni-app 为核心的 iOS 上架流程实践, 从构建到最终提交的完整路径
后端
喵手2 小时前
我在openEuler上从零开始构建云原生AI应用
后端
解读玫瑰2 小时前
WSL+openEuler嵌入式开发实战:交叉编译与QEMU仿真全流程
后端
Stream2 小时前
加密与签名技术之数字签名算法
后端
程序员爱钓鱼2 小时前
Node.js 编程实战:理解 Buffer 与 Stream
后端·node.js·trae