std::move can allow the efficient transfer of resources from object to to object. Andreas Fertig reminds us that using std::move inappropriately can make code less efficient.
In this article, 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 you should suggest std::move
yourself (in most cases). However, move semantics is way bigger than what this article covers, so don’t expect a full guide to the topic.
Looking More Closely |
For a deeper dive, see ‘Nothing is better than copy or move’ [Orr18]. C++11 introduced ‘move semantics’ to facilitate transferring the contents of one object to another more efficiently than creating a copy and then erasing the original. This is particularly focused on optimising the performance of temporary objects, such as when passing them into or out of a function call. However, in all the discussions about copying and moving, it is easy to forget that not creating an object in the first place may be even more efficient. This can be something done by design choice, or an optimisation applied during compilation. For example, introduction of a temporary object by copying can be removed; this is is called ‘copy elision’ in C++ and has been permitted in the language for many years. C++17 adds some additional specification around the creation of temporary variables with the phrase ‘temporary materialization’. Rog’s presentation looks at some ‘worked examples’ of how this behaves in practice, and some things to be aware of. |
The example in Listing 1 is the code I used to make my point: don’t use std::move
on temporaries! Plus, in general, trust the compiler and only use std::move
rarely. For this article, let’s focus on the example code.
class S {public: S() { printf("default constructor\n"); } ~S() { printf("deconstructor\n"); } // Copy constructor ① S(const S&) { printf("copy constructor\n"); } // Move constructor ② S(S&&) { printf("move constructor\n"); }};void Use(){ S obj{ S{} // Creating obj with a temporary of S ③ };} |
Listing 1 |
Here we see a, well, perfectly movable class. I left the assignment operations out. They are not relevant. Aside from the constructor and destructor, we see in ① the copy constructor and in ② the move constructor. All special members print a message to identify them when they are called.
Further down in Use
, we see ③, 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 movable members). The output I expect, and I wanted to show my participants, is:
default constructor move constructor deconstructor deconstructor
However, the resulting output was:
default constructor deconstructor
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
.
void Use() { S obj{ // Moving the temporary into obj std::move(S{}) }; }
This change indeed leads to the desired output:
default constructor move constructor deconstructor deconstructor
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 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 std::move
, in C++14 mode shows the expected output:
default constructor move constructor deconstructor deconstructor
If we now switch to C++17 as the standard, the output is once again:
default constructor deconstructor
Due to the mandatory copy elision in C++17, the compiler must elide this nonsense construction even with -fno-elide-constructors
. However, if we apply std::move
to the temporary copy, elision doesn’t apply anymore, and we’re back to 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.
References
[Orr18] Roger Orr, ‘Nothing is better than copy or move’ presentation given at ACCU 2018, available at: https://youtu.be/-dc5vqt2tgA
Andreas Fertig is a trainer and lecturer on C++11 to C++20, who presents at international conferences. Involved in the C++ standardization committee, he has published articles (for example, in iX) and several textbooks, most recently Programming with C++20. His tool – C++ Insights (https://cppinsights.io) – enables people to look behind the scenes of C++, and better understand constructs.
This article was published on Andreas Fertig’s blog in February 2022 (https://andreasfertig.blog/2022/02/why-you-should-use-stdmove-only-rarely/).