C++ moves for people who don’t know or care what rvalues are 🏘️
Moves in C++ don’t require understanding of deep technical juju to get a grasp on.
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++:
The most important thing about moves in C++ 🎉
#1: Moving a value doesn’t “move” anything.
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.
Things to have a basic understanding of before we go forward:
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)
A humble integer 🔧
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 different happened?
#2: Unless the type has special operations for moving object, a move is just a copy.
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!”¹
A slightly less humble String class 🎻
Strings are incredibly 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
char values, and then put the values
h, e, l, l, 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
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?
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.
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.
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.
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.
- 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:
- 1: 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. More.
- 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.