C++ Beyond the Syllabus #7: Move Semantics Pt. 3 — std::move Explained (Simply)

Transferring ownership of an lvalue’s resources.

Jared Miller
7 min readJul 25, 2024

Not a Medium member? View this entire article here!

This article is a part of the C++ Beyond the Syllabus series. Subscribe here to receive each new issue directly in your inbox.

In this article, I’ll cover everything you need to know about how to use (and when not to use!) std::move effectively.

But first, keep in mind this one thing…

The name of the function std::move is misleading and it doesn’t move anything. In fact, std::move only does one thing: unconditionally cast its argument to an rvalue.

The STL authors really should have called it something like std::cast_to_rvalue.

Learn These First

A Brief Recap

Recall the following definitions:

  • lvalues — have a name and address
  • rvalues — are temporary values that disappear after the expression they appear in
  • lvalue & rvalue references — bind to value types according to the following graphic:
  • transfer of ownership — a concept where the responsibility of managing dynamically allocated data members (i.e. — lives on the heap) is transferred from one object to another
  • lvalue copy constructor & copy-assignment operators — special member functions to create deep copies of the copied-from object’s dynamically allocated resources
  • rvalue copy constructor & copy-assignment operators — special member functions to claim ownership (i.e. — facilitates the transfer of ownership) of the copied-from object’s dynamically allocated resources

Now recall that the function stubs of these lvalue/rvalue special member function overloads differ only by the type category of the argument they accept. For an object, Object, the lvalue overloads may accept a const or non-const lvalue reference denoted by Object&. The rvalue overloads will accept an rvalue reference of type Object&&.

A Strong Foundation

If any of the above topics seem unfamiliar to you, I’d strongly recommend checking out the previous two articles in this series to ensure a strong foundation:

I myself have made the mistake, along with many of my peers, of trying to understand std::move without a strong foundation in the fundamentals. Everyone who does that will most definitely use std::move incorrectly, sometimes making code less efficient, and need to circle back to the basics anyways.

Finally Understanding Move Semantics

Congrats! If you’ve made it this far, I’m going to assume you have a solid foundation in the topics listed above. Now let’s finally get to understanding std::move.

The good news is it’s actually super simple.

Most tutorials take a long time to comprehend because they strictly explain it in non-intuitive technical terms. I’ll do better than that.

Explain It Simply, Please.

We know rvalue copy constructor & copy-assignment operators have two unique properties compared to their lvalue counterparts. Rvalue overloads of these special member functions:

  1. facilitate the transfer of ownership of dynamically allocated resources from the argument (i.e. — the copied-from object); and
  2. can only be invoked on rvalue arguments, as rvalue references may only bind to rvalues.

This is great and all, but what happens when we want to transfer ownership of dynamically allocated resources from an lvalue?

This is the use case for std::move.

But wait! This is where a lot of people get tripped up… Before we get any further, I want you to disassociate the concept of lvalues and rvalues from their full technical meanings. Yep — for the rest of this subsection, just forget that lvalues can have their address taken, rvalues disappear after the expression they appear in, and all the other nuances.

Instead, internalize these (very much true, but narrower different) definitions:

  • lvalue — a type that when copied, has a deep copy performed
  • rvalue — a type that when copied, has a transfer of ownership (of any dynamically allocated resources) performed

Note that the text “when copied” above really just means “when passed into the copy constructor or copy-assignment operator of another instance of the same object type.”

Now, simply treat std::move as a black box function that unconditionally converts the argument into an rvalue (in the context of the above definitions) for the remainder of the expression it appears in.

Let’s look at an example:

Assume we have a class, Object, that correctly implements the Rule of Five — meaning it definitely has lvalue and rvalue overloads of the copy constructor and copy-assignment operator.

If an lvalue, lvalue, of type Object is passed into the copy constructor or copy-assignment operator, it will invoke the lvalue overload causing its resources to be deep copied into the new object. In code, that would look something like this:

Object new_object(lvalue);

or this:

Object some_object;
some_object = lvalue;

If instead std::move(lvalue) is passed into the Object copy constructor or copy-assignment operator, it will invoke the rvalue overload causing lvalue’s resources to be transferred to the new object. This is because std::move(lvalue) casts lvalue to an rvalue for the remainder of the expression and per our narrow definitions above, rvalues indicate that we want resources to be transferred during copies. In code, that would look something like this:

Object new_object(std::move(lvalue));

or this:

Object some_object;
some_object = std::move(lvalue);

We should also clarify that invoking std::move on an rvalue is idempotent. It is valid code, but it doesn’t actually do anything, because it just casts an rvalue to an rvalue.

That wasn’t too hard, right?

A Bit More Technical

You can now remember the holistic definitions of lvalue and rvalue again, just don’t forget your non-technical understanding from above!

When introducing lvalues and rvalues, we talked about how you can always take the address of an lvalue, but not an rvalue. I also mentioned the caveat that the whole idea of lvalues and rvalues are really just rules for how the compiler needs to interface with our code, but behind the scenes it can make (almost) any optimizations it wants. This means that sometimes rvalues are stored in memory too. You still can’t take the address of them from code, but it’s important to note that an rvalue can represent some object stored in memory.

std::move itself does not move anything, copy anything, or even produce any executable code. The magic happens at compile time.

The rvalue generated by some code std::move(lvalue) still refers to the original object stored in memory that lvalue represents. One side-effect (or really non-effect) of this, is that a const lvalue passed into std::move will maintain its constness and result in a new value type called rvalue-to-const. This type isn’t used very often and its binding rules are pretty intuitive, but it’s worth knowing and adding to our handy binding chart:

By (statically) casting an lvalue to an rvalue, std::move simply affects overload resolution. All it does is cause the compiler to prefer an rvalue overload of the aforementioned special member functions, if one is defined.

This is a crazy powerful feature that allows us to rid our code of unnecessary copies. But…

With Great Power, Comes Great Responsibility

There are three big red flags to keep in mind when using move semantics:

  1. When we first introduced the rvalue copy constructor and copy-assignment operator, we noted that after invoking one of these functions (i.e. — moving from some object), the state of any dynamically allocated members in that moved-from object is only guaranteed to be “valid and destructible.” That definition is rather elusive, as it relies on the object’s implementation.

    Where does that leave us?

    Once you use std::move on an object, you shouldn’t make any assumptions regarding the state of moved-from data members based on properties observed prior to the move. In fact, a good rule of thumb is to either never access moved-from data members again or ensure they are reassigned with some expected state prior to being used again.
  2. Never use std::move in a return statement from a function (eg. return std::move(lvalue)), as this prevents the compiler from employing return value optimization (RVO), which is even more efficient than move semantics.
  3. Make sure any custom objects you use std::move on follow the Rule of Five and have rvalue overloads of the copy constructor and copy-assignment operator implemented. You definitely don’t want to assume you are using move semantics when you actually are just performing deep copies.

What’s Next?

Congrats! You now have enough knowledge to use std::move correctly… and also to point out existing mistakes in your code base 😎

You might be wondering how std::move is implemented. Well, that requires an understanding of perfect forwarding, which we’ll go over next week.

If you liked this article, please consider clapping and sharing. To get notified when the next “C++ Beyond the Syllabus” article is posted, you can subscribe for free here.

Sources & More Info

A special thanks to Mason Wilie (LinkedIn) who took time to edit and review this post.

--

--

Jared Miller
Jared Miller

Written by Jared Miller

A C++ Software Engineer in high frequency trading. Excited about low-latency code, distributed systems, and education technology.

Responses (1)