Reputation: 1110
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?
Upvotes: 32
Views: 15279
Reputation: 218750
I wouldn't do it this way. The reason for the move members to exist in the first place is performance. Doing this for your move constructor is like shelling out megabucks for a super-car and then trying to save money by buying regular gas.
If you want to reduce the amount of code you write, just don't write the move members. Your class will copy just fine in a move context.
If you want your code to be high performance, then tailor your move constructor and move assignment to be as fast as possible. Good move members will be blazingly fast, and you should be estimating their speed by counting loads, stores and branches. If you can write something with 4 load/stores instead of 8, do it! If you can write something with no branches instead of 1, do it!
When you (or your client) put your class into a std::vector
, a lot of moves can get generated on your type. Even if your move is lightning fast at 8 loads/stores, if you can make it twice as fast, or even 50% faster with only 4 or 6 loads/stores, imho that is time well spent.
Personally I'm sick of seeing waiting cursors and am willing to donate an extra 5 minutes to writing my code and know that it is as fast as possible.
If you're still not convinced this is worth it, write it both ways and then examine the generated assembly at full optimization. Who knows, your compiler just might be smart enough to optimize away extra loads and stores for you. But by this time you've already invested more time than if you had just written an optimized move constructor in the first place.
I note with some amusement that the referenced MSDN article no longer advises to write the move constructor in terms of the move assignment operator. And yet, it still gets it sort of wrong...
MemoryBlock(MemoryBlock&& other) noexcept
: _data(nullptr)
, _length(0)
{
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
(I've omitted logging and redundant comments)
Why not just initialize in the initialization list instead of assignments within the constructor body? Like this:
MemoryBlock(MemoryBlock&& other) noexcept
: _data(other._data)
, _length(other._length)
{
other._data = nullptr;
other._length = 0;
}
I haven't bothered to see if the optimized code is equivalent or not. I would guess that a decent optimizer could make the above two constructors have identical object code. But the second is easier to read and easier to write, with no other downsides.
The referenced MSDN article has a copy assignment operator that does not meet the basic exception safety guarantee:
MemoryBlock& operator=(const MemoryBlock& other)
{
if (this != &other)
{
// Free the existing resource.
delete[] _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
If _data = new int[_length];
throws an exception, then *this
has a _data
that points to a deleted block of memory, and a _length
with the value other._length
. Thus *this
is in a state that does not meet the class invariants:
MemoryBlock
invariants:
_data
is nullptr
or points to a new int[_length]
-allocated block of memory._data == nullptr
then _length == 0
.In addition to this correctness bug, there is also a performance bug:
If _length == other._length
, there is no need to go through the very expensive operation of deallocating a block of memory of length _length
just to turn around and allocate a block of memory of length _length
.
This is a more complicated copy assignment operator, but:
It carries the basic exception safety guarantee. If an exception happens, the object is left in a valid state (class invariants hold).
It avoids expensive trips to the heap at all costs. Branches are cheaper than trips to the heap, except for delete nullptr
.
MemoryBlock& operator=(const MemoryBlock& other)
{
if (this != &other)
{
if (_length != other._length)
{
// Free the existing resource.
delete[] _data;
// Set *this to a valid non-owning state
data_ = nullptr;
_length = 0;
if (other._length > 0)
{
// Set *this to own a block of length other._length
_data = new int[other._length]; // might throw
_length = other._length;
}
}
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
Upvotes: 15
Reputation: 88656
[...] 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.
Upvotes: 18
Reputation: 7
I would simply eliminate member initialization and write,
MemoryBlock(MemoryBlock&& other)
{
*this = std::move(other);
}
This will always work unless the move assignment throw exceptions, and it typically doesn't!
Advantages of this styles:
I think @Howard's post does not quite answer this question. In practice, classes often don't like copying, a lot of classes simply disable copy constructor and copy assignment. But most classes can be moveable even if they are not copyable.
Upvotes: -2
Reputation: 156
My C++11 version of the MemoryBlock
class.
#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));
}
With a correct C++11 compiler, MemoryBlock::MemoryBlock(size_t)
should only be called 3 times in the test program.
Upvotes: 4
Reputation: 37132
It depends what your move assignment operator does. If you look at the one in the article you linked to, you see in part:
// Free the existing resource.
delete[] _data;
So in this context, if you called the move assignment operator from the move constructor without initialising _data
first, you would end up trying to delete an uninitialized pointer. So in this example, inefficient or not, it's actually crucial that you do initialize the values.
Upvotes: 0
Reputation: 1704
I don't think you're going to notice significant performance difference. I consider it good practice to use the move assignment operator from the move constructor.
However I would rather use std::forward instead of std::move because it's more logical:
*this = std::forward<MemoryBlock>(other);
Upvotes: 1