繁体   English   中英

通过调用Move Assignment运算符实现Move构造函数

[英]Implementing Move Constructor by Calling Move Assignment Operator

MSDN文章“ 如何:编写移动构造器 ”具有以下建议。

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

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

通过双重初始化MemoryBlock的值,此代码是否效率低下,还是编译器能够优化掉多余的初始化? 我是否应该始终通过调用移动分配运算符来编写移动构造函数?

我不会这样。 移动成员首先存在的原因是性能 为您的移动构造函数执行此操作就像为超级跑车掏钱了百万美元,然后尝试通过购买常规汽油来省钱。

如果要减少编写的代码量,请不要编写move成员。 您的课程将在移动环境中很好地复制。

如果您希望代码具有高性能,请调整您的move构造函数并尽可能快地分配move分配。 好的搬家成员会很快发展起来,您应该通过计算负荷,仓库和分支机构来估计他们的速度。 如果您可以用4个加载/存储(而不是8个)来写东西,那就做吧! 如果您可以写没有分支而不是1的东西,那就去做吧!

当您(或您的客户)将类放入std::vector ,可以在您的类型上产生很多动作。 即使您的移动以8个负载/存储的速度闪电般快速 ,但如果仅以4或6个负载/存储的速度将其速度提高一倍,甚至快50%,恕我直言,这是值得花费的时间。

我个人讨厌看到等待的游标,并且愿意多花5分钟时间来编写我的代码,并且知道它要尽可能快。

如果您仍然不确定这样做是否值得,请用两种方式编写它,然后在完全优化的情况下检查生成的程序集。 谁知道,您的编译器可能足够聪明,可以为您优化多余的负载和存储。 但是到现在,您已经投入了比最初编写优化的move构造器更多的时间。

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

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

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

是的,只要通过移动赋值运算符实现它,除了在你测量 ,这导致次优性能的情况下。


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

让我们来看一些组装! 这将显示来自Microsoft网站的准确代码,其中包含两个版本的move构造函数:手动和通过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构造函数生成最佳代码。 现在,当您需要在两个位置更改代码,或者需要调试一个奇怪的错误(仅由于仅在一个位置更改代码而发生)时,现在进行无用的微优化将花费您大量的开发时间。 而且这浪费了开发时间,本来可以花在进行有用的优化上。

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

#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次。

我认为您不会注意到明显的性能差异。 我认为,最好使用move构造函数中的move赋值运算符。

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

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

这取决于您的移动分配操作员的工作。 如果您查看链接到的文章中的文章,您会看到部分内容:

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

因此,在这种情况下,如果不先初始化_data 从move构造函数调用了move赋值运算符,则最终将试图删除未初始化的指针。 因此,在此示例中,不管效率低与否,初始化值都是至关重要的。

我将简单地消除成员初始化并编写,

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

除非移动分配抛出异常,否则它将始终有效,并且通常不会!

这种样式的优点:

  1. 您无需担心编译器是否会双重初始化成员,因为在不同的环境下情况可能有所不同。
  2. 您编写的代码更少。
  3. 即使以后再向该类中添加其他成员,也不需要更新它。
  4. 编译器通常可以内联移动分配,因此复制构造函数的开销将降至最低。

我认为@Howard的帖子并未完全回答这个问题。 实际上,类通常不喜欢复制,许多类只是禁用复制构造函数和复制分配。 但是大多数类即使不可复制也可以移动。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM