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:

A table shows variables a and b starting as a question mark. a becomes 6, then b becomes 6. Visualization made in http://pythontutor.com/cpp.html#mode=edit

Note that most of these images going forwards are animated, so don’t whiz past them.

Let’s move it!

Animation showing b being moved into a, in exactly the same manner as before.

But nothing different happened?

Exactly.

#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:

A block inside “Stack” saying “myString” with key/value pairs of “length: 5”, and “text” has a line with arrow pointing to a separate block of memory. This block is named “Heap” and shows 5 boxes, numbered 0,1,2,3,4, containing values that spell out “hello”.

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.²

We used new to get enough space to put 5 different 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”.

As above, except there is a second stack object, myCopiedString, and a second heap array, which starts as “hello”, then becomes “jello”.

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 *myString.text to *myCopiedString.text .

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.text .

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.

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.

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. 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.

To recap: