首页 > 编程语言 >关于C++函数返回值的拷贝优化问题

关于C++函数返回值的拷贝优化问题

时间:2024-08-22 11:17:06浏览次数:8  
标签:函数 对象 C++ 返回值 拷贝 移动 优化 构造函数

在传统C++程序中,如果函数的返回值是一个对象的话,可能需要对函数中的局部对象进行拷贝。如果该对象很大的话,则程序的效率会降低。
在C++ 11以后,出现的移动语义(Move Semantic)及拷贝优化(Copy Elision)都是解决这个问题的方法。
本文试图以一个最简单的例子来说明这个问题。

案例
下面来看一个简单的例子(这里的BigObj类的实例假设是一个需要很大存储空间的大对象):

#include <iostream>

using std::cout;
using std::endl;


class BigObj
{
public:
    BigObj()
    {
        cout << "这是默认构造函数" << endl;
    }
    
    BigObj(const BigObj& that)
    {
        cout << "这是拷贝构造函数" << endl;
    }
    
    BigObj(BigObj&& that)
    {
        cout << "这是移动构造函数" << endl;
    }
    
    ~BigObj()
    {
        cout << "这是析构函数" << endl;
    }
};


BigObj fun()
{
    BigObj obj = BigObj();
    return obj;
}

int main()
{
    BigObj obj = fun();
    return EXIT_SUCCESS;
}

拷贝优化

运行该程序,我们会得到如下输出:

1 这是默认构造函数
2 这是析构函数

可以发现fun()函数在返回BigObj对象的时候没有进行拷贝,这是由于编译期帮我们做了拷贝优化。
移动语义
但是编译器堆函数返回值的拷贝优化并不是能完全实现的,有一些特殊情况下会失效。所以比较保险的做法是定义移动构造函数,当没有拷贝优化的时候可以通过移动语义避免低效的拷贝。
我们可以通过-fno-elide-constructors关闭编译器的拷贝优化,下面是对应的cmake文件:

cmake_minimum_required(VERSION 3.26)
project(CxxTutorial)

set(CMAKE_CXX_STANDARD 23)
#SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-elide-constructors")

add_executable(CxxTutorial main.cpp)

通过配置关闭拷贝优化以后,我们执行上面的程序,输出结果如下:

1 这是默认构造函数
2 这是移动构造函数
3 这是析构函数
4 这是析构函数

可以看到关闭拷贝优化以后,在定义了移动构造函数的时候,函数返回零时对象的时候会调用移动构造函数,转义所有权,减少数据拷贝。但是移动构造也会生成一个新的对象,所以输出结果中会调用两次析构函数,第一次析构函数是析构了函数中定义的零时对象,第二次是析构了函数返回值返回后的对象。

那如果我们没有定义移动构造函数,而且编译期也没有进行拷贝优化程序的运行会是怎么样的呢?
注释掉上面的移动构造函数,我们可以看到输出结果如下:

1 这是默认构造函数
2 这是拷贝构造函数
3 这是析构函数
4 这是析构函数

这个结果是在预料之中的,没有拷贝优化,没有移动构造函数的情况下,程序会调用拷贝构造函数。假设这个对象是一个大对象,则拷贝过程会花费一些时间,降低了程序的执行效率。而使用移动语义的话,直接转义对象的所有权,效率会高一些。

结论
对于C++函数返回一个大对象的时候,在编译器能进行拷贝优化的时候,会优先进行返回值的拷贝优化。如果不能进行拷贝优化,在有定义移动构造函数的时候,则会调用移动构造函数进行返回值对象所有权转义,减少不必要的拷贝。最后,这两种情况失效的时候,才会调用拷贝构造函数进行对象的深拷贝。

有了上述结论,我们在写程序的时候最佳实践是函数返回值可以直接返回函数体内定义的零时对象,但是我们需要在定义该对象的时候实现移动构造函数。这样就可以保证函数的返回值要么有编译器拷贝优化,要么会调用移动构造函数减少拷贝开销。

标签:函数,对象,C++,返回值,拷贝,移动,优化,构造函数
From: https://www.cnblogs.com/peifx/p/18373422

相关文章

  • C++学习,运算符
    运算符是告诉编译器执行特定数学或逻辑函数的符号。C++语言内置运算符丰富,提供以下类型的运算符:算术运算符关系运算符逻辑运算符按位运算符赋值运算符其它运算符 算术运算符(ArithmeticOperators)下表显示了C++语言支持的所有算术运算符。假设变量A=10,变量B=20:操作......
  • C++学习,数据类型
    编写程序时,需要使用各种变量来存储信息,变量是用于存储值的内存。创建变量时,在内存中保留了一些空间。存储各种数据类型的信息,如字符,宽字符,整数,浮点,双浮点,布尔等。根据变量的数据类型,系统分配内存并决定可以存储的内容。 内置类型C++提供了丰富的内置和用户定义的数据类......
  • C++ queue(STL queue,队列)用法详解
    只能访问queue<T>容器适配器的第一个和最后一个元素。只能在容器的末尾添加新元素,只能从头部移除元素。许多程序都使用了queue容器。queue容器可以用来表示超市的结账队列或服务器上等待执行的数据库事务队列。对于任何需要用FIFO准则处理的序列来说,使用queue容器适......
  • c++高精度细剖
    深入剖析C++中的高精度计算是一个广泛且深入的主题,它涵盖了多种技术和策略,用于处理超过标准整数或浮点数类型能表示范围的数值。在这里,我将提供一个概括性的框架,涵盖高精度计算的基本概念、常见方法、实现细节以及可能的应用场景,但请注意,由于篇幅限制,这里无法直接达到“十万字......
  • C++ wsl2 ubuntu 环境配置
    目前学习C++,配合Ubuntu进行开发,IDE使用Clion,这里记录一下环境准备WSL2C++一般是用在linux下,这里就用Ubuntu进行开发,考虑到window系统,这里准备用wsl2.虚拟化wsl2要系统支持虚拟化,一般在bios中进行处理,成功之后,任务管理器-->性能适用于Linux的Windows子系统wsl更新ws......
  • C++——STL——vector容器
    vector的头文件#include<vector>vector的声明与初始化vector<类型>变量=赋值;//整型vector<int>a={1,2,3,4};//浮点型 vector<double>b={1.1,2.2,3.2,4.4};//字符型 vector<char>c={'a','b','c'......
  • 「字符串」前缀函数|KMP匹配:规范化next数组 / LeetCode 28(C++)
    概述为什么大家总觉得KMP难?难的根本就不是这个算法本身。在互联网上你可以见到八十种KMP算法的next数组定义和模式串回滚策略,把一切都懂得特别混乱。很多时候初学者的难点根本不在于这个算法本身,而是它令人痛苦的百花齐放的定义。有的next数组从0下标开始,有的从1开始;有的表......
  • 引发C++程序内存泄漏的常见原因分析与排查方法总结
    目录1、概述2、内存泄漏与程序的位数3、调用哪些接口去动态申请内存?4、引发内存泄漏的常见原因总结4.1、通过malloc/new等动态申请的内存,在使用完后,没有调用free/delete去释放(也可能是调用了上面讲到的HeapAlloc或VirtualAlloc等API接口)4.2、函数调用者调用内部申请内存......
  • 掌握C++中的std::list:高效处理插入与删除的最佳选择
    在C++标准模板库(STL)中,std::list是一个非常重要的容器,属于序列式容器。与std::vector和std::deque不同,std::list是一个双向链表(doublylinkedlist),其设计更适合于频繁的插入和删除操作,而不是随机访问。本文将深入探讨std::list的实现原理、使用场景以及与其他容器的对比......
  • 昇腾 - AscendCL C++应用开发 线程安全的队列
    昇腾-AscendCLC++应用开发线程安全的队列flyfishC++mutex各种各样的互斥锁mutex、timed_mutex、recursive_mutex、shared_mutexC++线程间同步的条件变量std::condition_variable和std::condition_variable_anyC++提供的智能指针unique_ptr、shared_ptr、wea......