Why you should use std::move only rarely
In today's post, I try to tackle a topic that comes up frequently in my classes, move semantics, and when to use
std::move. I will explain to you why not say
std::move yourself (in most cases).
As already said, move semantics is a topic that comes up frequently in my classes, especially the part when to use
std::move. However, move semantics is way bigger than what today's post covers, so don't expect a full guide to move semantics.
The example below is the code I used to make my point: don't use
std::move on temporaries! Plus, in general, trust the compiler and use
std::move only rarely. For this post, let's focus on the example code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Here we see a, well, perfectly moveable class. I left the assignment operations out. They are not relevant. Aside from the constructor and destructor, we see in A the copy constructor and in B the move constructor. All special members print a message to identify them when they are called.
Further down in
Use, we see C, a temporary object of
S used to initialize
obj, also of type
S. This is the typical situation where move semantics excels over a copy (assuming the class in question has moveable members). The output I expect, and I wanted to show my participants, is:
1 2 3 4
However, the resulting output was:
Performance-wise the output doesn't look bad, but it doesn't show a move construction. The question is, what is going on here?
This is the time to apply
At this point, somebody suggestion was to add
1 2 3 4 5 6
This change indeed leads to the desired output:
1 2 3 4
It looks like we just found proof that
std::move is required all the time. The opposite is the case!
std::move makes things worse here. To understand why, let's first talk about the C++ standard I used to compile this code.
Wait a moment!
In C++14, the output is what I showed you for both Clang and GCC. Even if we compile with
-O0 that doesn't change a thing. We need the
std::move to see that the move constructor is called. The key here is that the compiler can optimize the temporary away, resulting in only a single default construction. We shouldn't see a move here because the compiler is already able to optimize it away. The best move operation will not help us here. Nothing is better than eliding a certain step. Eliding is the keyword here. To see what is going on, we need to use the
-fno-elide-constructors flag, which Clang and GCC support.
Now the output changes. Running the initial code, without the
std::move in C++14 mode shows the expected output:
1 2 3 4
If we now switch to C++17 as the standard, the output is once again:
Due to the mandatory copy elision in C++17, even with
-fno-elide-constructors, the compiler must now elide this nonsense construction. However, if we apply
std::move to the temporary copy elision doesn't apply anymore, and we're back in seeing a move construction.
You can verify this on Compiler Explorer godbolt.org/z/G1ebj9Yjj
The take away
That means, hands-off! Don't move temporary objects! The compiler does better without us.