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

Ell*_*tch 17 c++ move-constructor move-semantics c++11

MSDN文章如何:编写移动构造函数具有以下建议.

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

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

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

How*_*ant 14

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

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

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

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

就个人而言,我厌倦了看到等待游标,并愿意捐出额外的5分钟来编写我的代码并且知道它尽可能快.

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

  • 这里有一些很好的建议,但我认为这个答案忽略了考虑到重复代码的成本超过仅花了5分钟写第二次 - 重复代码的成本比可读性,可维护性和鲁棒性.特别是当存在重复代码时,它会引发一种非常常见的错误类型:即,在代码的一个实例中修复或更改某些内容而不在另一个实例中.在我的生活中被咬过的次数比我所关心的多,我不会重复代码,除非有一个*非常强大的证明理由. (17认同)
  • 性能是原因之一,但不是唯一或最重要的原因.大多数情况下,当我编写移动构造函数和移动赋值运算符是因为我的类具有资源的唯一所有权.通常性能不是问题,因此在这些情况下避免重复是有意义的. (5认同)
  • Donald Knuth写道:"程序员浪费了大量的时间来思考或担心程序中非关键部分的速度,而这些效率尝试实际上在考虑调试和维护时会产生很大的负面影响.我们应该忘记小效率,大约97%的时间说:过早的优化是所有邪恶的根源.然而,我们不应该把这个关键的3%的机会放弃." (4认同)

Luk*_*odt 11

[...] 编译器能够优化掉额外的初始化吗?

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

我应该总是通过调用移动赋值运算符来编写移动构造函数吗?

是的,只需通过移动赋值运算符来实现它,除非您测量到它会导致性能欠佳。


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

让我们看看一些组装!显示了来自 Microsoft 网站的确切代码以及两个版本的移动构造函数:手动和通过移动分配。这是 GCC 的汇编输出-O-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
Run Code Online (Sandbox Code Playgroud)

除了正确版本的附加分支外,代码完全相同。含义:重复的分配已被删除

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


这个 get 重复了很多次,但很重要:不要过早地进行微优化!

不要误会我的意思,我也讨厌由于懒惰或草率的开发人员或管理决策而浪费大量资源的软件。而节约能源不仅仅是关于电池,也是一个我非常热衷的环保话题。但是,过早地进行微优化在这方面无济于事!当然,将大数据的算法复杂性和缓存友好性放在脑后。但在进行任何特定优化之前,请先衡量!

在这种特定情况下,我什至猜测您将永远不必手动优化,因为编译器始终能够围绕您的移动构造函数生成最佳代码。现在进行无用的微优化将花费您以后的开发时间,当您需要在两个地方更改代码或需要调试一个奇怪的错误时,该错误仅因为您只在一个地方更改了代码而发生。这是浪费的开发时间,而本可以花在做有用的优化上。