When I was first learning about move semantics in C++, I kept reading articles that explained in terms of other scary sounding jargon — lvalues, rvalue references,
memcpy, ownership. None of these things are strictly necessary to know about to understand the core of move semantics. (Though, the more you learn about them, the greater your understanding of move semantics will become.)
You may have heard of move semantics, and may know that they’re “faster”, but not why, or even how to move something. (Here “moves” and “move semantics” mean the same thing.)
This article will deliberately simplify or ignore some concepts (like constructors, rvalue references, stack vs heap) to make the core idea of moving easier to follow, so don’t worry if you already know this stuff and see something that isn’t technically correct. I’ll mark clarifications for these with a number. This article is aimed at those writing everyday (non-library) code, with little to no existing understanding of move semantics, to help get over the initial conceptual hurdle.
Now let’s look at the most important thing about moves in C++:
There. That is by far the biggest hurdle to overcome. In its simplest form, a move is just a copy. In its best form, a move is an optimized way to copy values that you don’t want to keep around any more.
You will need to be aware of:
- Pointers (that they are memory addresses, that are dereferenced)
- C style arrays (think of a block of letters next to each other in memory)
- The stack vs the heap (local function variables, vs variables that live across functions)
- Class constructors (that you can write them yourself, and that “copy constructors” exist)
In this very simple case, the value of
a is copied into
b. In memory, that would look like this animation:
Note that most of these images going forwards are animated, so don’t whiz past them.
Let's move it!
But nothing happened?
All primitive types — your integers, floats, pointers, and some others— do not explicitly “move”. There is no way to move a primitive that is quicker than copying it.
What's std::move doing?
std::move takes in any value, and says “Hey — mark this as movable for now!”
Strings are notoriously difficult to get right in programming languages. We’re going to implement a dumb simple string class, that consists of a pointer to some block of chars in memory, and a length.
To save on unnecessary complexity, I won’t be sharing code for the constructor or other parts yet — they’d only distract from the main point.
Let’s see what this looks like in memory:
Stack? Heap? 📚
As a quick refresher, variables within a function live on the stack in memory. Anything created behind the new keyword will live on the heap, which exists across all function calls.
new to get enough space to put 5 different char values, and then put the values
o in those spaces.
Let’s copy the string
To be clear: We want to make an entirely new copy, that we can edit and do what we want to, without affecting the first. In fact, we’ll update our copied version to say “jello” instead of “hello”.
Awesome! 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
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.
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
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.
I regularly no longer need variables — should I move them all?
There are some cases where moving can actually stop certain compiler optimizations. 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 characters
You’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.
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:
- Not using a move will always be safe, just potentially less performant.
- 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_ptrin the first place!)
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. Here’s a neat short explanation of them.
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.
- 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.
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: