我应该删除内部创建线程的类的复制构造函数和赋值运算符吗?

Posted

技术标签:

【中文标题】我应该删除内部创建线程的类的复制构造函数和赋值运算符吗?【英文标题】:Should I delete a copy constructor and an assignment operator of a class that internally creates threads? 【发布时间】:2018-01-07 13:58:08 【问题描述】:

当一个类拥有线程和互斥对象时,是否存在任何潜在的危险情况,即复制/赋值是危险的,这意味着应该删除复制构造函数和赋值?

考虑这个示例代码:

class A : B

    std::thread t;
    std::mutex m;

  public:
    A() : B() 
    virtual ~A()
    
        if (t.joinable())
            t.join();
    

    // Should I delete cctor and assignment operator?

    virtual void Method()
    
        t = std::thread([this] 
        
            std::lock_guard<std::mutex> lck(m);
            ... // processing
        );
    

    // Other methods that lock on mutex m
;

如果我理解正确,Method() 中创建的线程在A 之外将不可见,这意味着使用默认 cctor 进行复制应该没有问题,因为整个状态都会被复制。我的推理正确吗?

【问题讨论】:

如果复制没有逻辑意义,那么=delete执行复制操作是明智之举。 因为您有 std::threadstd::mutex 作为成员,它们将被隐式删除。 threadmutex 已经删除了复制,所以无论如何您的课程都无法复制。但明确可能是好的,特别是如果你不确定自己。 :-) 谢谢@RichardCritten 和 BoPersson,确实是这样,很抱歉我错过了。如果您将其移至答案,我会接受。 @RichardCritten 线程删除了它的复制构造函数but not the assignment operator。不过没关系,因为 mutex 会删除两者。 【参考方案1】:

任何(扩展)状态包含指向自身的指针的类都必须具有已删除的复制/移动,或者必须编组该状态。

t = std::thread([this] 

上面的行存储了一个指向类扩展状态this的指针。

因此默认复制和移动是不合适的。删除是一种选择;仔细且可能昂贵地编组另一个。

另外,thread 和 mutex 都是只能移动的类型。所以副本会被隐式删除。

但是,您的移动分配/构造将由编译器编写并且错误。所以删除它们或修复它们。


没有类值类型的语言(如 Java/C#)有一个习语,即有一个带状态的类和一个在该状态上工作的线程。在像 C++ 这样以价值为中心的语言中,这是一个糟糕的计划。将您的状态存储在外部(例如,共享或唯一 ptr),与您的线程共享(作为共享 ptr 或作为观察 ptr),然后突然默认移动变得有意义。

如果不这样做,您的对象就会变得固定 - 无法安全移动 - 这会削弱许多优秀的 C++ 习惯用法,或强制外部智能指针包装。

【讨论】:

【参考方案2】:

复制构造函数和赋值都需要删除或定义。如果你想删除它们,不要做任何事情:C++ 会为你隐式删除它们。

您的班级有std::mutex 作为成员。它的复制构造函数和赋值运算符被删除。这也使得 C++ 删除类的复制构造函数和赋值运算符,因为为它们生成默认行为需要调用已删除的成员。

注意: 删除不是您唯一的选择:如果您发现复制 A 或使其可赋值很有用,您可以选择定义复制构造函数和赋值运算符创建一个新线程和一个新互斥体,并复制对象的相关部分以支持您认为有用的语义。尽管默认情况下 C++ 不会为您执行此操作,但您可以选择手动执行此操作。

【讨论】:

以上是关于我应该删除内部创建线程的类的复制构造函数和赋值运算符吗?的主要内容,如果未能解决你的问题,请参考以下文章

派生自std :: exception的类的赋值运算符

如何为具有自引用指针的类实现复制构造函数/赋值运算符?

c++ 类的复制函数

c++中拷贝构造函数和赋值运算符重载本质上一样么

编译器何时为类的特殊成员提供定义?

初始化列表:复制构造函数和赋值运算符 = 冗余?