We created a new object³, myCopiedString, on the stack.
We then created a new array of characters for it on the heap, and copied each of the characters one by one from *myString.
text to *myCopiedString.
Note, in order to do all this, I had to implement a custom copy constructor on the String class.
I’ll link to this code later, as it’s not required to see right now.
Let’s move it!Before we do, it is important to know that I have also implemented a move constructor onto String now.
This is different from the copy constructor above.
Without a move constructor, our code will fallback to the copy constructor.
Again, it is not important to see the code for the move constructor yet, just know that it will be called if the type is marked as movable.
As above, simply wrapping the value in std::move() will do the trick.
Two stack objects, myString and myMovedString, and a single heap array “hello”.
A line is drawn from myString.
text to the array.
Then, a line is drawn from myMovedString.
text to the array.
Then the first line from myString is removed, and replaced with NULL.
A lot happened ????The key thing to note here is that "hello" never moved or got copied itself.
We did not copy all 5 characters over to a new place in memory this time.
Instead, we made myMovedString.
text point directly at myString.
You may also have noticed that myString.
length was then set to 0, and myString.
text to nullptr.
This is important to the point of moves.
#3: Use an explicit move to say “I won’t use this object after this move.
”I regularly no longer need variables — should I move them all?No.
If writing regular, non-library code, it’s likely you never need to write std::move.
Instead, as long as your types have move constructors, the compiler will find the best places to attempt moves.
In particular, do NOT wrap your return value in std::move in a function — in many cases, this is actually slower than returning directly.
I don’t understand why we care about the cost of copying 5 measly charactersYou’re right, doing a copy of "hello" will take a negligible amount of time.
But what if instead of copying that, we had to copy the entire text value of The Lord Of The Rings when we didn’t need to?Or if instead of a String class, we had an array of LargeExpensiveToCopyObjects?.In these cases, simply copying a pointer and updating a length value is clearly much faster.
Another case to consider is while copying 5 characters once may not seem a lot, it’s easy to copy 5 characters across 100 places in a codebase.
Using moves where we know it is safe to do so can help save us from “death by a thousand cuts” style performance issues.
Why did myString get set to zero and null? 0️⃣We didn’t have to touch myString at all, however we explicitly set it to some clearly incorrect state⁴.
This is because we have moved it, essentially saying to the compiler and other programmers “I never want to use this variable again.
”Importantly, we have to consider double deletion of pointers.
Long story short, calling delete on the same piece of memory twice will crash your program.
If we have two Strings pointing at the same piece of memory, and both destruct and try to delete their own pointers — boom, you have a double delete and a crash.
Also consider nulling as a signal to other programmers.
If someone else were now to accidentally use myString, they’d likely very quickly crash and realize they weren’t meant to.
If we hadn’t set it to an incorrect state, we would now have two separate editable Strings pointing at the same piece of memory.
Some very weird and hard to track down bugs would likely arise from this being the case.
Where are move semantics used?Mostly where-ever ownership of an object needs to be transferred.
If this sounds like a wishy-washy answer, I’m sorry I can’t (read: won’t for brevity) go into much further detail about ownership here, but I encourage you explore and learn about ownership and lifetimes.
⁵Unique pointers are a good example of something that “owns” some piece of heap memory, and that ownership can only be transferred elsewhere by moving it.
Efficient sorting algorithms like those provided by the C++ standard library will also make use of moves internally for faster swaps.
In some specific cases, having move constructors on expensive objects can aid performance, as the standard library and compiler can spot places to best use a move rather than a copy.
As always, don’t rely on this blindly and profile your code if you need to be faster.
If you’re not sure whether to use a move or not, at least of one of these two cases will be true:1) Not using a move will always be safe, just potentially less performant.
2) Your compiler will complain you’re trying to copy a movable-only object (like a unique_ptr) and you’ll have to move anyway (or you didn’t want a unique_ptr in the first place!)#4: Use moves to transfer ownership of an object, either for semantic or performance reasons.
What is std::move really doing?I never promised I wouldn’t mention rvalue references.
This is where superscript¹ points to.
You can consider std::move(myString) to be loosely equivalent to static_cast<String&&>(myString) — it casts from type T to type T&&.
This is known as an rvalue reference.
When I said movable earlier, I actually just meant it was an rvalue reference type.
I won’t explain more here, but hopefully this provides a good starting point for you.
To conclude ☄️Move semantics as a concept are simpler than the jargon around them would suggest.
I hope this explanation has given you a grounding in what move semantics are for and how they work.
Further areas to explore after this might be std::swap, return value optimization (RVO), and the rule of five.
To recap:Moving a value doesn’t “move” anything.
Unless the type has special operations for moving object, a move is just a copy.
Use an explicit move to say “I won’t use this value after this move.
”Use moves to transfer ownership of an object, either for semantic or performance reasons.
Thanks to Simon Brand, Jon Holmes and James Thomas for proofreading and providing invaluable feedback & help ????All visualizations were created in the fantastic PythonTutor for C++ web tool.
You can see the tool visualizing this example here.
Note how it uses a special emoji to signify already deleted data.
You can find the code used to perform the copy and move here:Footnotes1: As seen above, movable actually refers to rvalue references.
This guide has more information on them.
2: The stack is known as automatic storage.
The heap is dynamic storage.
The new keyword does not always allocate with dynamic storage, though that is the usual implementation.
3: The term object in C++ has a specific definition, that at a high level just means “a variable”.
We don’t mean object in the “object oriented” sense.
4: I say a moved object will be in an incorrect state afterwards.
Specifically, it will be in a valid but unspecified state — it will still be part of correct, well defined behavior C++ code, but the value might just now be semantically useless.
5: Ownership (in my own experience at least) is a concept that seems nebulous and imprecise until you suddenly “get” it.
I like this article on the subject.
It clicked for me while I was learning Rust, which has very strict and explicitly defined lifetime and ownership semantics.