通过调用移动赋值运算符实现移动构造函数

时间:2013-06-14 22:36:12

标签: c++ c++11 move-semantics move-constructor

MSDN文章How to: Write a Move Constuctor具有以下建议。

  

如果为类提供移动构造函数和移动赋值运算符,则可以通过编写来消除冗余代码   移动构造函数来调用移动赋值运算符。该   以下示例显示了移动构造函数的修订版本   调用移动赋值运算符:

// Move constructor.
MemoryBlock(MemoryBlock&& other)
   : _data(NULL)
   , _length(0)
{
   *this = std::move(other);
}

通过双重初始化MemoryBlock的值,这个代码是否效率低下,或者编译器是否能够优化掉额外的初始化?我应该总是通过调用移动赋值运算符来编写移动构造函数吗?

6 个答案:

答案 0 :(得分:14)

我不会这样做。移动成员首先存在的原因是性能。为你的移动构造函数执行此操作就像为超级汽车敲出megabucks然后试图通过购买普通汽油来节省资金。

如果你想减少你编写的代码量,就不要写移动成员。您的课程将在移动环境中复制。

如果您希望代码具有高性能,请定制移动构造函数并尽可能快地移动赋值。好的移动成员将非常快,你应该通过计算负载,商店和分支来估计他们的速度。如果你能用4个加载/存储而不是8来写东西,那就去做!如果你可以写一些没有分支而不是1的东西,那就去做吧!

当您(或您的客户)将您的课程放入std::vector时,您的类型可以生成很多的动作。即使您在8次装载/存储时移动闪电,如果您可以将速度提高两倍,甚至只需要4或6次装载/存储就可以提高50%,那么花费时间就可以了。

就个人而言,我厌倦了看到等待游标,并愿意多花5分钟来编写我的代码并知道它尽可能快。

如果您仍然不相信这是值得的,请双向编写,然后在完全优化时检查生成的程序集。谁知道,你的编译器可能足够聪明,可以为你优化掉额外的负载和存储。但到了这个时候,你已经投入了更多的时间,而不是你刚刚编写了一个优化的移动构造函数。

答案 1 :(得分:3)

MemoryBlock类的我的C ++ 11版本。

#include <algorithm>
#include <vector>
// #include <stdio.h>

class MemoryBlock
{
 public:
  explicit MemoryBlock(size_t length)
    : length_(length),
      data_(new int[length])
  {
    // printf("allocating %zd\n", length);
  }

  ~MemoryBlock() noexcept
  {
    delete[] data_;
  }

  // copy constructor
  MemoryBlock(const MemoryBlock& rhs)
    : MemoryBlock(rhs.length_) // delegating to another ctor
  {
    std::copy(rhs.data_, rhs.data_ + length_, data_);
  }

  // move constructor
  MemoryBlock(MemoryBlock&& rhs) noexcept
    : length_(rhs.length_),
      data_(rhs.data_)
  {
    rhs.length_ = 0;
    rhs.data_ = nullptr;
  }

  // unifying assignment operator.
  // move assignment is not needed.
  MemoryBlock& operator=(MemoryBlock rhs) // yes, pass-by-value
  {
    swap(rhs);
    return *this;
  }

  size_t Length() const
  {
    return length_;
  }

  void swap(MemoryBlock& rhs)
  {
    std::swap(length_, rhs.length_);
    std::swap(data_, rhs.data_);
  }

 private:
  size_t length_;  // note, the prefix underscore is reserved.
  int*   data_;
};

int main()
{
   std::vector<MemoryBlock> v;
   // v.reserve(10);
   v.push_back(MemoryBlock(25));
   v.push_back(MemoryBlock(75));

   v.insert(v.begin() + 1, MemoryBlock(50));
}

使用正确的C ++ 11编译器,MemoryBlock::MemoryBlock(size_t)只应在测试程序中调用3次。

答案 2 :(得分:3)

  

[...]编译器将能够优化掉多余的初始化吗?

几乎在所有情况下:是的。

  

我是否应该总是通过调用移动分配运算符来编写移动构造函数?

是的,只需通过移动分配运算符来实现它,除非您测量会导致次优性能。


当今的优化器在优化代码方面做得非常出色。您的示例代码特别容易优化。首先:几乎在所有情况下都将内联move构造函数。如果您通过移动分配运算符来实现它,那么该函数也会被内联。

让我们看一下组装程序! This显示了Microsoft网站上带有以下两个版本的move构造函数的确切代码:手动和通过移动分配。这是带有-O的GCC的汇编输出(-O1具有相同的输出; clang的输出得出相同的结论):

; ===== manual version =====           |   ; ===== via move-assig =====
MemoryBlock(MemoryBlock&&):            |   MemoryBlock(MemoryBlock&&):
    mov     QWORD PTR [rdi], 0         |       mov     QWORD PTR [rdi], 0
    mov     QWORD PTR [rdi+8], 0       |       mov     QWORD PTR [rdi+8], 0
                                       |       cmp     rdi, rsi
                                       |       je      .L1
    mov     rax, QWORD PTR [rsi+8]     |       mov     rax, QWORD PTR [rsi+8]
    mov     QWORD PTR [rdi+8], rax     |       mov     QWORD PTR [rdi+8], rax
    mov     rax, QWORD PTR [rsi]       |       mov     rax, QWORD PTR [rsi]
    mov     QWORD PTR [rdi], rax       |       mov     QWORD PTR [rdi], rax
    mov     QWORD PTR [rsi+8], 0       |       mov     QWORD PTR [rsi+8], 0
    mov     QWORD PTR [rsi], 0         |       mov     QWORD PTR [rsi], 0
                                       |   .L1:
    ret                                |       rep ret

除了用于正确版本的其他分支外,代码完全相同。含义:重复的作业已被删除

为什么要增加分支? Microsoft页面定义的移动分配运算符比移动构造函数执行更多的工作:它可以防止自我分配。 move构造函数不受此保护。 但是:正如我已经说过的那样,几乎在所有情况下都将内联构造函数。在这种情况下,优化器可以看到它不是自我分配,因此该分支也将被优化。


此操作已重复多次,但很重要:请勿过早进行微优化!

别误会,我也讨厌由于懒惰或草率的开发人员或管理决策而浪费大量资源的软件。节约能源不仅与电池有关,还与环保有关,我对此非常感兴趣。 但是,过早进行微优化在这方面没有帮助!当然,请把算法的复杂性和大数据的缓存友好性放在脑后。但是在进行任何特定的优化之前,请先测量!

在这种特定情况下,我什至猜测您将不必手动优化,因为编译器将始终能够围绕move构造函数生成最佳代码。现在,如果您需要在两个位置更改代码,或者需要调试仅在一个位置更改代码才发生的奇怪错误,那么现在进行无用的微优化将花费您大量的开发时间。而且这浪费了开发时间,本来可以花在进行有用的优化上。

答案 3 :(得分:1)

我认为你不会注意到显着的性能差异。我认为从移动构造函数中使用移动赋值运算符是一种好习惯。

但是我宁愿使用std :: forward而不是std :: move,因为它更符合逻辑:

*this = std::forward<MemoryBlock>(other);

答案 4 :(得分:0)

这取决于您的移动分配操作员的作用。如果你看一下你链接的文章中的那个,你会看到:

  // Free the existing resource.
  delete[] _data;

因此,在此上下文中,如果您首先从移动构造函数调用移动赋值运算符而不初始化_data,则最终会尝试删除未初始化的指针。所以在这个例子中,效率低或不高,实际上初始化值是至关重要的。

答案 5 :(得分:0)

我会简单地消除成员初始化并写入,

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

除非移动赋值抛出异常,否则这将始终有效,而且通常不会!

这种风格的优点:

  1. 您无需担心编译器是否会对成员进行双重初始化,因为这可能会因不同环境而异。
  2. 你写的代码较少。
  3. 即使您将来在课程中添加额外的成员,也无需更新它。
  4. 编译器通常可以内联移动赋值,因此复制构造函数的开销很小。
  5. 我认为@ Howard的帖子并没有完全回答这个问题。在实践中,类通常不喜欢复制,很多类只是禁用复制构造函数和复制赋值。但是大多数课程即使不可复制也可以移动。