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

<think>我们有一个问题:如何在C++中检查类的所有成员变量是否为可复制类型(copyassignable)。这里我们关注的是“可复制赋值”(copyassignable),即类型是否支持赋值操作符(operator=)用于同类型对象的赋值。在C++中,我们可以使用类型特征(typetraits)来检查单个类型是否是可复制赋值的,通过`std::is_copy_assignable`。但是,对于类的所有成员变量,我们需要一种方法来遍历它们并分别检查。然而,C++没有内置的反射机制来自动遍历类的成员。因此,我们需要手动检查每个成员,或者使用一些技巧来自动化,但这通常需要预先知道所有成员。这里提供几种方法:1.**手动检查每个成员**:对于已知的类,我们可以显式地检查每个成员的类型。2.**使用C++20的concepts**:如果我们使用C++20,可以编写一个concept来检查。3.**静态断言与类型特征结合**:我们可以写一个静态断言来检查每个成员。由于我们不知道类的具体结构,我们只能提供一种方法,让用户按照这种方法来检查自己的类。以下是一个示例,展示如何手动检查一个类的所有成员是否都是可复制赋值的:```cpp#include<type_traits>classMyClass{public:inta;doubleb;std::stringc;};static_assert(std::is_copy_assignable_v<decltype(MyClass::a)>,"Memberaisnotcopyassignable");static_assert(std::is_copy_assignable_v<decltype(MyClass::b)>,"Memberbisnotcopyassignable");static_assert(std::is_copy_assignable_v<decltype(MyClass::c)>,"Membercisnotcopyassignable");```但是,如果成员是私有的,我们就无法这样访问,这时我们需要在类内部定义类型或者使用友元等机制,或者通过公共的getter来获取类型,但这样很麻烦。另一种方法是利用类的赋值操作符的生成行为:如果一个类没有显式定义拷贝赋值运算符,那么编译器会自动生成一个,这个自动生成的拷贝赋值运算符会递归地对每个成员进行拷贝赋值。因此,如果所有成员都是可拷贝赋值的,那么整个类也是可拷贝赋值的(除非有const成员或引用成员,因为这两种成员不能被赋值)。因此,我们可以通过检查这个类本身是否是拷贝赋值类型来间接判断,使用`std::is_copy_assignable`。但是注意:即使所有成员都是可拷贝赋值的,如果类显式删除拷贝赋值运算符,那么类也不是可拷贝赋值的。同时,如果存在不能被拷贝赋值的成员(如const成员、引用成员),那么自动生成的拷贝赋值运算符会被定义为删除的。所以,我们通常直接检查类本身:```cpp#include<type_traits>classMyClass{inta;doubleb;std::stringc;//如果有引用成员或const成员,则拷贝赋值运算符会被删除//int&ref;//如果有引用成员,则拷贝赋值运算符被删除//constintc;//同理};static_assert(std::is_copy_assignable_v<MyClass>,"MyClassisnotcopyassignable");```这种方法简单有效,但如果我们想要知道是哪个成员导致不可拷贝赋值,我们就需要手动检查每个成员。如果我们想要在编译时获得更详细的错误信息(比如哪个成员不可拷贝赋值),我们可以结合使用多个静态断言(对于公有成员),或者利用一些模板技巧,但这比较复杂。另外,C++11之后,我们可以使用`std::tuple`来模拟反射,如果我们将所有成员封装为一个元组,那么就可以通过遍历元组的方式来检查每个成员的类型是否可拷贝赋值。但前提是我们需要将类转换为元组的形式。例如,我们可以这样:```cppclassMyClass{public:inta;doubleb;std::stringc;//定义一个元组类型,包含所有成员autoas_tuple()const{returnstd::tie(a,b,c);}};```然后,我们可以写一个模板函数来检查元组中的每个类型:```cpptemplate<typenameT,typename=void>structis_tuple_elements_copy_assignable:std::false_type{};template<typename...Args>structis_tuple_elements_copy_assignable<std::tuple<Args...>>:std::conjunction<std::is_copy_assignable<Args>...>{};//然后我们可以这样使用:static_assert(is_tuple_elements_copy_assignable<decltype(std::declval<MyClass>().as_tuple())>::value,"Notallmembersarecopyassignable");```但是这种方法要求我们定义一个`as_tuple`函数,而且如果成员是私有的,我们还需要提供访问权限。综上所述,最简单实用的方法是直接检查整个类是否可拷贝赋值,因为它反映了成员综合的效果。如果我们希望精确到每个成员,并且类结构已知且成员是公有的,我们可以手动检查每个成员。根据引用[1]中提到的命名规范,成员变量通常以尾随下划线表示(如member_),但这不是强制性的。在我们的示例中,我们按照通常方式命名。另外,引用[2]提到C++中可以通过引用传递参数,这可能与我们的问题关系不大。引用[3]提到了使用声明,我们这里也不直接相关。现在,我们可以总结一下:如果我们需要检查一个类的所有成员是否都是可拷贝赋值的,我们可以:1.如果类没有显式声明拷贝赋值运算符,且没有不可拷贝赋值的成员(如const成员、引用成员),则`std::is_copy_assignable_v<Class>`为true。2.如果类提供了`as_tuple`方法,我们可以检查元组中的每个元素。3.手动检查每个成员(仅当成员是公有时)。下面是一个完整的示例,展示如何使用第一种方法:```cpp#include<type_traits>#include<string>classMyClass{public:inta;doubleb;std::stringc;//如果有一个const成员,则拷贝赋值运算符会被删除//constintd=0;//如果取消注释,static_assert就会失败};intmain(){static_assert(std::is_copy_assignable_v<MyClass>,"MyClassshouldbecopyassignable");return0;}```如果取消注释const成员d,那么编译将会失败。因此,回答用户的问题:在C++中,检查类的所有成员变量是否为可复制类型(这里指可复制赋值)的最直接方法是使用`std::is_copy_assignable`来检查整个类。如果类是可复制赋值的,那么它的所有非静态非const非引用成员都是可复制赋值的。如果要定位到具体哪个成员不可复制赋值,则需要手动检查每个成员的类型。注意:可复制赋值(copyassignable)和可复制构造(copyconstructible)是两个不同的概念。这里我们只讨论赋值。生成的相关问题:1.如何检查一个类是否是可复制构造的?2.在C++中,如何检查类成员是否具有移动赋值操作符?3.如何检查一个类型是否是triviallycopyable?4.如果类中有不可复制的成员,如何使类仍然可复制?5.如何让编译器生成默认拷贝赋值运算符?</think> 在 C++ 中检查类的所有成员变量是否为可复制赋值类型,需要使用类型特征(type traits)和静态断言(static_assert)。以下是实现方法及示例: ### 核心方法 1. **使用 `std::is_copy_assignable` 类型特征** ```cpp #include <type_traits> class MyClass { public: int a; double b; std::string c; // 检查所有成员类型 static_assert(std::is_copy_assignable_v<decltype(a)>, "Member a not copy assignable"); static_assert(std::is_copy_assignable_v<decltype(b)>, "Member b not copy assignable"); static_assert(std::is_copy_assignable_v<decltype(c)>, "Member c not copy assignable"); }; ``` 2. **自动化检查(C++17 折叠表达式)** 需将成员封装为元组(需 C++17 支持): ```cpp #include <tuple> #include <type_traits> class MyClass { int a_; double b_; std::string c_; public: auto members_as_tuple() const { return std::tie(a_, b_, c_); } // 检查元组所有元素类型 static_assert([]{ using TupleType = decltype(std::declval<MyClass>().members_as_tuple()); return std::conjunction_v< std::is_copy_assignable<std::tuple_element_t<0, TupleType>>, std::is_copy_assignable<std::tuple_element_t<1, TupleType>>, std::is_copy_assignable<std::tuple_element_t<2, TupleType>> >; }(), "Not all members are copy assignable"); }; ``` ### 注意事项 1. **不可复制赋值的类型**: - 引用类型(`int&`) - `const` 限定成员(`const int`) - 带有删除 `operator=` 的类(如 `std::mutex`) 2. **检查整个类** 直接检查类本身更简单,但会掩盖具体失败原因: ```cpp static_assert(std::is_copy_assignable_v<MyClass>, "Class not copy assignable"); ``` ### 完整示例 ```cpp #include <iostream> #include <type_traits> #include <string> #include <mutex> class CopySafeClass { public: int x; std::string s; static_assert(std::is_copy_assignable_v<decltype(x)>, "x not copy assignable"); static_assert(std::is_copy_assignable_v<decltype(s)>, "s not copy assignable"); }; class ProblemClass { public: std::mutex mtx; // std::mutex 不可复制赋值 // 编译报错: static_assert failed due to requirement ... }; int main() { static_assert(std::is_copy_assignable_v<CopySafeClass>, "Safe"); // 通过 static_assert(!std::is_copy_assignable_v<ProblemClass>, "Unsafe"); // 通过 } ``` ### 关键原因说明 1. **`std::is_copy_assignable`** 检测类型是否有有效的 `operator=`,包括编译器隐式生成的版本[^2]。 2. **成员访问限制** 若成员为 `private`,需在类内部声明检查逻辑,或通过友元/访问器暴露类型[^1]。 3. **引用与 `const` 成员** 引用成员和 `const` 成员会隐式删除类的拷贝赋值运算符[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值