C++ – Implementing Move Constructor by Calling Move Assignment Operator

cc++11move-constructormove-semantics

The MSDN article, How to: Write a Move Constuctor, has the following recommendation.

If you provide both a move constructor and a move assignment operator for your class, you can eliminate redundant code by writing
the move constructor to call the move assignment operator. The
following example shows a revised version of the move constructor that
calls the move assignment operator:

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

Is this code inefficient by doubly initializing MemoryBlock's values, or will the compiler be able to optimize away the extra initializations? Should I always write my move constructors by calling the move assignment operator?

Best Answer

[...] will the compiler be able to optimize away the extra initializations?

In almost all cases: yes.

Should I always write my move constructors by calling the move assignment operator?

Yes, just implement it via move assignment operator, except in the cases where you measured that it leads to suboptimal performance.


Today's optimizer do an incredible job at optimizing code. Your example code is especially easy to optimize. First of all: the move constructor will be inlined in almost all cases. If you implement it via move assignment operator, that one will be inlined as well.

And let's look at some assembly! This shows the exact code from the Microsoft website with both versions of the move constructor: manual and via move assignment. Here is the assembly output for GCC with -O (-O1 has the same output; clang's output leads to the same conclusion):

; ===== 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

Apart from the additional branch for the right version, the code is exactly the same. Meaning: duplicate assignments have been removed.

Why the additional branch? The move assignment operator as defined by the Microsoft page does more work than the move constructor: it is protected against self-assignment. The move constructor is not protected against that. But: as I already said, the constructor will be inlined in almost all cases. And in these cases, the optimizer can see that it's not a self assignment, so this branch will be optimized out, too.


This get's repeated a lot, but it's important: don't do premature micro-optimization!

Don't get me wrong, I also hate software that wastes a lot of resources due to lazy or sloppy developers or management decisions. And saving energy is not just about batteries, but also an environmental topic, which I am very passionate about. But, doing micro-optimizations prematurely doesn't help in that regard! Sure, keep the algorithmic complexity and cache friendliness of your large data in the back of your head. But before you do any specific optimization, measure!

In this specific case, I would even guess that you will never have to hand optimize, because the compiler will always be able to generate optimal code around your move constructor. Doing the useless micro-optimization now will cost you development time later when you need to change code in two places or when you need to debug a strange error that only happens because you changed code in only one place. And that is wasted development time that could rather be spent doing useful optimizations.