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 the copy constructor in A and the move constructor 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:
1 2 |
|
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 std::move
, right?
At this point, somebody's suggestion was to add std::move
:
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 always required. 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:
1 2 |
|
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.
Andreas