C++不可复制对象作为成员变量导致默认复制构造被删除

一、问题描述

假设现在有个类如下:

class TransParamRecorder {
public:
    TransParamRecorder(std::string save_path);
    ~TransParamRecorder();
    bool openFile();
    bool closeFile();
    bool record(const geometry_msgs::TransformStamped& trans, int64_t timestamp);
private:
    std::ofstream file_;  //文件流对象
    std::string save_path_; //文件保存路径
};

这个类看起来没问题,该类有构造函数、析构函数,一些方法和私有成员。
好,那现在我们写了一些代码来使用这个类,很简单,如下:

...
 recorder_list.emplace_back(TransParamRecorder(savePath));
...

进行编译后报错了。
核心错误是这样的:

error: use of deleted function ‘TransParamRecorder::TransParamRecorder(const TransParamRecorder&)’

仔细一看,报错信息中提到了一个函数TransParamRecorder::TransParamRecorder(const TransParamRecorder&),说是这个函数被删除了,不让用。

这是什么函数?
这是默认的复制构造函数

有用到这个函数吗?
理论上没用到,因为emplace_back是原地构造的,目的就是要避免产生拷贝。但是实际上这里用到了,待会再讲这个问题。

这不是默认的复制构造函数吗?怎么就变成被删除的函数了呢?
这时我联想到禁用复制构造函数的一种方式:

默认的复制构造函数需要调用类的析构函数来销毁复制的对象。如果类的析构函数是私有的,编译器将无法生成默认的复制构造函数

构造单例模式时会用到这种方式,不过我们检查之后,很容易能够发现析构函数并没有被声明为私有的。
说明这里面有另一套机制导致默认复制构造函数被干掉了,这套机制也就是下面要介绍的不可复制对象。

二、C++不可复制对象

有几种情况下,成员变量可能被标记为不可复制。
对象拥有指针成员并使用了浅拷贝:如果类中包含指针成员,并且使用了默认的浅拷贝方式进行复制,那么复制后的对象和原对象将共享同一块内存,这可能导致释放重复的内存、访问无效指针等问题。为了避免这种情况,需要自定义复制构造函数,并使用深拷贝方式来复制指针成员。

对象拥有资源管理类成员:如果类中包含了拥有资源(如文件句柄、互斥锁、网络连接等)的成员变量,并且资源管理类没有实现复制操作,那么默认的复制构造函数将无法复制这些成员变量。为了确保正确的资源管理,需要自定义复制构造函数,并根据具体情况来处理资源的复制或共享。

对象拥有被删除的复制构造函数的成员变量:如果类中包含了复制构造函数被删除的成员变量,那么默认的复制构造函数也会被删除。这可能是由于成员变量的类定义了私有的复制构造函数、删除的复制构造函数,或者不可复制的基类等情况。

很显然,在上面设计的类中有一个资源管理类成员std::ofstream file_。
因此这块碰上第二类情况。

三、解决问题

这事不难解决,默认拷贝构造被删了,那就手动写一个呗:

TransParamRecorder(const TransParamRecorder& other);
TransParamRecorder& operator=(const TransParamRecorder& other);

然后实现过程完善一下,这样就解决了。

四、有用拷贝构造函数吗?

刚刚留了一个疑点,emplace_back是原地构造的,目的就是要避免产生拷贝,为什么说这里又用到拷贝构造函数了呢?这不是和我们学的不一样了?(学了个锤子)
实际上,编译器在编译过程中并不会认为emplace_back一定要原地构造。
https://en.cppreference.com/w/cpp/container/vector/emplace_back中有提到:

-T (the container’s element type) must meet the requirements of MoveInsertable and EmplaceConstructible.

我来结合这俩名词解释一番,理解的不一定对,但是解释得通哈:

  • MoveInsertable 概念要求在元素插入时,容器需要能够执行移动语义(Move Semantics),即移动元素而不是拷贝元素。这是为了提高性能,避免不必要的拷贝操作。如果元素类型具有移动构造函数,则可以直接使用移动构造函数将元素插入容器,而不需要调用拷贝构造函数。
  • 然而,如果元素类型没有移动构造函数,编译器将尝试使用拷贝构造函数代替。因此,在这种情况下,元素类型必须满足 EmplaceConstructible 的要求,即能够通过给定的参数构造新的元素对象。

所以意思就是:在这块儿编译器确实可以并且实际上也正是在尝试调用拷贝构造函数。

搞清楚了这套逻辑,另一套解决方案也就有了:加一个移动构造函数呗。有了显示的移动构造函数,编译器自然不去碰拷贝构造函数这个备胎了,岂不美哉。
不过这编译器如果有其它想法呢,在某种不为人知的 优化策略下编译器突然就想找找拷贝构造函数,有没有可能呢?我个人感觉最好把移动构造和拷贝构造都加上,保险一点。


参考:
https://stackoverflow.com/questions/40457302/c-vector-emplace-back-calls-copy-constructor
https://blog.csdn.net/weixin_45880571/article/details/119450328
https://en.cppreference.com/w/cpp/container/vector/emplace_back

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值