最佳C++移动构造函数实现实践

10
我正在尝试理解移动构造函数的实现。我们都知道,如果需要在C++类中管理资源,我们需要实现五个规则(C++编程)。
Microsoft给出了一个例子:https://msdn.microsoft.com/en-us/library/dd293665.aspx 这里有一个更好的例子,使用复制交换来避免代码重复:Dynamically allocating an array of objects
     // C++11
     A(A&& src) noexcept
         : mSize(0)
         , mArray(NULL)
     {
         // Can we write src.swap(*this);
         // or (*this).swap(src);
         (*this) = std::move(src); // Implements in terms of assignment
     }

在移动构造函数中,直接:

         // Can we write src.swap(*this);
         // or (*this).swap(src);

我认为(*this) = std::move(src)稍微有点复杂。因为如果我们不小心写成(*this) = src,它会调用普通的赋值运算符而不是移动赋值运算符。

除了这个问题,在微软的示例中,他们编写了以下代码:在移动赋值运算符中,我们需要检查自我赋值吗?这种情况可能发生吗?

// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
   std::cout << "In operator=(MemoryBlock&&). length = " 
             << other._length << "." << std::endl;

   if (this != &other)
   {
      // Free the existing resource.
      delete[] _data;

      // Copy the data pointer and its length from the 
      // source object.
      _data = other._data;
      _length = other._length;

      // Release the data pointer from the source object so that
      // the destructor does not free the memory multiple times.
      other._data = nullptr;
      other._length = 0;
   }
   return *this;
}

1
移动构造函数对我来说看起来很傻。为什么不在初始化列表中初始化所有内容,然后将src设置为有效的移动状态?这应该最多需要4个赋值操作,如果您不关心更改移动对象的大小,则只需3个。 - NathanOliver
1
在移动构造函数中,可以直接使用 src.swap(*this); 或者 (*this).swap(src);。假定你已经实现了适合的 swap 方法,我认为这样做是没有问题的。 - Igor Tandetnik
1
@MaximEgorushkin:我忘记链接到http://wg21.link/lwg2468。 - Jonathan Wakely
@NathanOliver,你是对的。这样写是为了避免代码重复。这样更容易维护。 - Dongguo
@Igor Tandetnik,谢谢 :) - Dongguo
显示剩余2条评论
2个回答

10

一种方法是实现默认构造函数、拷贝构造函数和 swap 函数。

然后使用前三个实现移动构造函数、拷贝赋值运算符和移动赋值运算符。

E.g.:

struct X
{
    X();
    X(X const&);
    void swap(X&) noexcept;

    X(X&& b)
        : X() // delegate to the default constructor
    {
        b.swap(*this);
    }

    // Note that this operator implements both copy and move assignments.
    // It accepts its argument by value, which invokes the appropriate (copy or move) constructor.
    X& operator=(X b) {
        b.swap(*this);
        return *this;
    }
};

如果您一直在使用C++98中的这个习语,那么一旦添加移动构造函数,您就可以获得移动赋值而无需编写任何代码。

在某些情况下,这种习惯用法可能不是最有效的。因为复制操作符总是首先构造一个临时对象,然后与其交换。通过手动编写赋值运算符,可能可以获得更好的性能。如果有疑问,请检查优化后的汇编输出并使用分析器。


6

我也在网上寻找实现移动构造函数和移动赋值运算符的最佳方法。目前有一些方法,但没有一个是完美的。

以下是我迄今为止的发现。

这是一个我用作示例的Test类:

class Test {
private:
  std::string name_;
  void*       handle_ = nullptr;

public:
  Test(std::string name)
    : name_(std::move(name))
    , handle_(malloc(128))
  {
  }
    
  ~Test()
  {
    if(handle_) free(handle_);
  } 

  Test(Test&& other) noexcept;             // we are going to implement it
  Test& operator=(Test&& other) noexcept;  // we are going to implement it

  void swap(Test& v) noexcept
  {
    std::swap(this->handle_, v.handle_);
    std::swap(this->name_, v.name_);
  }

private:
  friend void swap(Test& v1, Test& v2) noexcept
  {
    v1.swap(v2);
  }
};

方法一:直截了当

Test::Test(Test&& other) noexcept
  : handle_(std::exchange(other.handle_, nullptr))
  , name_(std::move(other.name_))
{
}

Test& Test::operator=(Test&& other) noexcept
{
  if(handle_) free(handle_);
      
  handle_ = std::exchange(other.handle_, nullptr);
  name_ = std::move(other.name_);

  return *this;
}

优点

  • 最佳性能表现

缺点

  • 移动构造函数和移动赋值函数存在代码重复
  • 部分析构函数的代码也在移动赋值函数中重复出现

方法二:销毁 + 构建

Test::Test(Test&& other) noexcept
  : handle_(std::exchange(other.handle_, nullptr))
  , name_(std::move(other.name_))
{
}

Test& Test::operator=(Test&& other) noexcept
{
  this->~Test();
  new (this) Test(std::move(other));

  return *this;
}

优点

  • 不会出现代码重复
  • 如果没有虚函数,性能良好

缺点

  • 虚函数表(VMT)会被初始化两次(如果类有虚函数)
  • 不能在基类中使用。基类必须只实现移动构造函数。

方法三:拷贝并交换(Copy'n'Swap)

Test::Test(Test&& other) noexcept
  : handle_(std::exchange(other.handle_, nullptr))
  , name_(std::move(other.name_))
{
}

Test& Test::operator=(Test&& other) noexcept
{
  Test (std::move(other)).swap(*this);
  return *this;
}

或者复制和移动运算符二合一:

Test& Test::operator=(Test other) noexcept
{
  swap(other, *this);
  return *this;
}

优点

  • 无代码重复

缺点

  • 创建了额外的对象
  • swap 函数内部交换数据成员时会创建额外的较小对象
  • swap 函数中存在某种形式的代码重复

方法 #4:通过移动赋值实现移动构造函数

这是您 @Dongguo 在 MSDN 上发现的。

Test::Test(Test&& other) noexcept
{
  *this = std::move(other);
}

Test& Test::operator=(Test&& other) noexcept
{
  if(handle_) free(handle_);
      
  handle_ = std::exchange(other.handle_, nullptr);
  name_ = std::move(other.name_);

  return *this;
}

优点

  • 没有代码重复

缺点

  • 不能用于包含非默认可构造数据成员的类。
  • 移动构造函数中的数据成员初始化两次。

链接

更多回答


你的所有代码都存在内存泄漏问题! - mada
@0x0 检查了所有4个解决方案,都没有泄漏。它在哪里? - Dmytro Ovdiienko
我认为你在某种程度上编辑了代码。请查看第一种方法,在移动赋值之前,handle_ = std::exchange(other.handle_, nullptr); 这行代码之前,应该删除 handle_,因为它指向一个临时的 Test 对象,你只是通过将其分配给 nullptr 来孤立这个内存。 - mada
或者简单地将您的代码放入MCVS中,并运行任何泄漏检查器,如_CrtDumpMemoryLeaks() - mada
@0x0 我没有对代码进行重大更改。只是用 malloc/free 代替了 close_handle(以便测试内存泄漏)。其余的代码没有任何变化。请查看更改历史记录。我使用了 MSVC Address Sanitizer 和 Visual Leak Detector。还检查了 _CrtDumpMemoryLeaks - 没有泄漏。代码在这里 [https://pastebin.com/Q6rhkLXq]。 - Dmytro Ovdiienko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接