【问题标题】:Default move constructor with mutex member具有互斥成员的默认移动构造函数
【发布时间】:2019-04-04 13:52:06
【问题描述】:

我有一个带有已删除复制构造函数的类,我正在尝试将互斥锁成员放入如下内容:

struct A {
    A(const A &other) = delete;
    A& operator=(const A &other) = delete;
    A(A&& other) = default;
    A& operator=(A &&other) = default;

    std::mutex lock;
};

编译器抱怨我试图调用已删除的复制构造函数,我总结这是由于 std::mutex 类型是不可移动的。如何让互斥锁成员以最少的麻烦与移动构造函数一起玩?我实际上并不想将互斥体成员本身移动到新构造的对象中,并且实际上希望每个移动的对象只构造它自己的互斥体

【问题讨论】:

  • 这可能很有趣:SO: Move constructor for std::mutex.
  • 我不确定 default 移动构造函数/赋值是否是个好主意。 lock 不应该负责保护一些东西吗?移动构造/分配不应该考虑移动对象的lock 吗?

标签: c++ move


【解决方案1】:

我实际上并不想将互斥体成员本身移动到新构造的对象中,并且实际上希望每个移动的对象只构造它自己的互斥体

然后简单地定义你的移动构造函数来构造一个新的互斥体:

struct A {
    A(const A &other) = delete;
    A& operator=(const A &other) = delete;
    A(A&& other)
        : lock()
    {
    }

    A& operator=(A &&other) = delete;

    std::mutex lock;
};

移动分配仍然是一个问题,可能应该被删除。除非你能回答这个问题:当你被分配一个新值时,现有的 mutex 成员会发生什么?特别是:如果在现有互斥​​锁被锁定时为您分配了一个新值怎么办?

【讨论】:

  • 我假设被分配一个新值应该只保留互斥锁:我了解 OP 的设计,以便互斥锁保护 A 的(其他)内容,因此将新内容复制/移动到 @987654323 @ 应该仍然是 this 的原始互斥体。
  • @Angew 我也认为这就是 OP 想要的。但是,我确实认为这具有潜在的危险。这里有一个std::mutex 成员的事实似乎表明该互斥锁应该保护一些关键部分。在互斥锁当前被锁定时,应该不可能只更改现有对象的值……
  • 确实如此,这基本上使我的回答无效。我将不得不添加一个警告。
  • @Angew 请添加它并保持我的支持。我确实认为包装器是一个很好的解决方案,可以准确地实现所要求的。只是开始时所要求的可能有问题……
  • 是的,这个想法是互斥锁保护 A 的其他成员,最终一个新的分配应该只有一个新的互斥锁成员。这种类型在互斥锁被锁定时不会被移动,尽管目前在代码中没有用语义表示,但是在其他答案中提到的将互斥锁锁定在移动构造函数中听起来适合安全
【解决方案2】:

作为为您的类提供自定义移动操作的替代方法,您可以创建一个通用包装器:

template <class T>
class PerObject
{
  T data;
public:
  PerObject() = default;
  PerObject(const PerObject&) {}
  PerObject& operator= (const PerObject&) { return *this; }
  T& operator* () const { return data; }
  T* operator-> () const { return &data; }
};

并像这样使用它:

struct A {
    A(const A &other) = delete;
    A& operator=(const A &other) = delete;
    A(A&& other) = default;
    A& operator=(A &&other) = default;

    PerObject<std::mutex> lock;
};

包装器的复制(和移动)操作是无操作的,因此包含包装器的对象将始终包含它开始时的那个。


警告:但是,根据您的班级使用互斥锁的方式,上述内容实际上可能很危险。如果互斥锁用于保护类中的其他数据,那么它可能在分配对象时被锁定,因此无论如何您都必须提供手动移动操作。在这种情况下,代码可能看起来像这样:

struct A {
    A(A&& other) : lock{}, /* anything else you need to move-construct */
    {
      // Note: it might even be necessary to lock `other.lock` before moving from it, depending on your class's exact semantics and expected use.
    }
    A& operator=(A &&other)
    {
      if (this == &other) return *this;  // Otherwise, double-locking would be possible

      // If you need to lock only this object:
      std::unique_lock<std::mutex> l(lock);
      // Alternatively, if you need to lock both objects:
      std::scoped_lock l(lock, other.lock);

      // Now move data from other to this

      return *this;
    }

    std::mutex lock;
};

【讨论】:

    【解决方案3】:

    一种方法是让你的移动构造函数在调用时创建新的mutex

     A(A&& other): lock()
     {
         //... move other things
     }
    

    您也可以将std::unique_ptr() 用于std::mutex,因为它是可移动的。

    struct A {
        A(const A &other) = delete;
        A& operator=(const A &other) = delete;
        A(A&& other) = default;
        A& operator=(A &&other) = default;
    
        std::unique_ptr<std::mutex> lock;
    };
    
    A::A() : lock(new std::mutex())
    

    使用这种方法,您不会在每次移动对象时都创建新的互斥体,这将消除一些开销。

    【讨论】:

    • 请注意,使用 unique_ptr 的语义与 OP 想要的有很大不同:“我实际上并不想将互斥体成员本身移动到新构造的对象中,实际上我想每个移动的对象只是构造它自己的互斥体"
    • @Angew 然后第一个选项有效。我只是给了另一种方式。
    • 那么你可能至少应该提到它与第一个的不同(以及与 OP 想要的不同)。
    猜你喜欢
    • 2016-01-27
    • 2016-08-08
    • 2017-01-10
    • 1970-01-01
    • 2018-10-28
    • 2012-09-01
    • 1970-01-01
    • 2016-12-16
    • 1970-01-01
    相关资源
    最近更新 更多