Move Semantics, std::move and rvalues

Learn how to implement move semantics in C++, the difference between lvalues and rvalues, and what std::move really does.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

73.jpg
Ryan McCombe
Ryan McCombe
Posted

In our previous lesson, we saw how we could implement copy constructors and operators to implement copy semantics. Here, we’re going to implement move semantics.

However, it’s helpful to get some brief historical context first. The idea of "moving things" in C++ doesn’t work how most people expect, so a quick overview of how it came about is pretty helpful to understand what’s going on.

Implementing Move Semantics in the Copy Constructor

The first thing to note about the move constructor is that it didn’t always exist. It was added to the C++ spec in 2011 (C++11).

However, the desire to “move” objects had existed for decades. Before 2011, one of the ways we implemented move semantics was within the copy constructor.

Imagine we have a large, complex object with many thousands of sub-objects, perhaps contained in collections like vectors. Creating a deep copy of this object would be an expensive process.

There are situations where we can be more efficient with this. Rather than deeply copying everything to the new object, we can just have our copy constructor create the new object by stealing the resources of the old:

struct MyStruct {
  MyStruct(MyStruct& Source) {
    Resources = Source.Resources;
    Source.Resources = nullptr;
  }
  Resource* Resources;
};

This would leave the old object in a dilapidated, semi-useless state. But, in situations where we weren’t going to use the old object again, that didn’t matter - we can just enjoy the performance gains.

A common use case for this is when we were returning the object by value from a function. Expending resources maintaining the integrity of the original object, given it was about to be deleted, seemed especially wasteful.

This process, where we quickly-but-destructively "copy" and object colloquially became known as “moving” the object. We didn’t move it - the old object is still there, it was just left in a deteriorated state, sometimes called the “moved-from” state.

Improving the Approach

Fundamentally, this was a hack, and it had some drawbacks. Most notably, the copy constructor is also called in situations where we really do want to create a deep copy of the object. For example, passing an object by value into a function also calls the copy constructor.

If our copy constructor was ruining our object for performance gains in a different context, that meant any time we pass our object into a function, it will also be ruined.

Eventually, C++11 gave us better options here, by giving us the ability to implement copying and moving as separate functions. However, from C++’s perspective, what copying and moving means is defined by us, the users.

The only thing the compiler is responsible for is deciding which constructor to call. What those functions do is up to us.

As a general practice, it’s perhaps easier to understand what’s going on if we don’t even think of our functions as copying and moving, but rather as two different ways to copy our objects:

  • A “slow” copy, that prioritizes preserving the integrity of the original object, even if it means the new object takes longer to create
  • A “fast” copy, that prioritizes creating the new object as quickly as possible, even if it means damaging the original object

Copy and Movement Semantic Interactions

Viewing movement as nothing more than a faster copy for situations where we don’t care about the original also helps us understand some of the behaviors we will see coming from the compiler. Most notably, the copy constructor can stand in for the movement constructor.

For example, if we try to “move” an object that doesn’t have a movement constructor, the compiler will not throw an error. It will just use the copy constructor instead.

After all, when we move something, we don't necessarily care if the old object is left in a dilapidated or pristine state, we only care about the new object. Both the copy and move constructors treat the new object with the same respect, so the copy constructor can safely stand in for a missing movement constructor. It just may run slightly slower than a movement constructor could.

The opposite is not true. When we copy something, we want the old object to still be usable afterward. The movement constructor isn’t intended for that - its main concern is creating the new object as quickly as possible, even if it means damaging the original object. So, it cannot be used instead of the copy constructor.

As such, if we try to copy an object, and all we have is a movement constructor, the compiler will throw an error

Lvalues and Rvalues

When we pass an object by value, the compiler needs to know whether it is safe to move the object, or it needs to perform a slower copy. The mechanism for deciding this is determining whether our function received an lvalue or an rvalue.

A simplification that helps understand the difference is to imagine lvalues are containers, and rvalues are the contents of the container. In the following example, x is an lvalue, and 5 is an rvalue:

int x { 5 };

Without a container, an rvalue doesn’t last long. Typically, it is lost right after the expression it is used in is evaluated.

In the following example, 1, 5, and 6 are all rvalues. After this line of code, the rvalues 1 and 5 are forgotten as they’re not in an lvalue container. This specific rvalue 6 is bound to the x lvalue container, so it remains available as long as x is available.

int x { 1 + 5 };

But how does this help us with move semantics? Imagine we have two function calls:

int x { 5 };

Function(x);
Function(5);

The first function call receives x. x is an lvalue - it continues to exist after our function call.

The second function call receives 5. 5 is an rvalue - it is forgotten after our function call.

As such, the second call to our function does not have to afford it's argument the same level of respect, because it's just an rvalue..

Lvalue and Rvalue References

Within our functions parameters, we previously saw how we could capture lvalue references using the & character:

void myFunc(int &x) {}

Whilst we didn’t draw attention to this nuance in the past, an error would be thrown if we attempted to pass an rvalue into an lvalue reference:

void myFunc(int &x) {}

int main() { myFunc(5); }
candidate function not viable:
expects an lvalue for 1st argument

This makes conceptual sense - by our function accepting a non-const reference, we are suggesting that it is going to modify it’s argument. But, it doesn’t make sense to modify an rvalue.

If we specify our argument as const, our code will compile successfully:

void myFunc(const int &x) {}

int main() { myFunc(5); }

We can explicitly create variables and parameters that accept rvalue reference. An rvalue reference is identified by two && characters.  Therefore:

  • int& is an lvalue reference to an int
  • int&& is an rvalue reference to an int

Let's see an example:

#include <iostream>

void myFunc(int& x) {
  std::cout << "lvalue\n";
}

void myFunc(int&& x) {
  std::cout << "rvalue\n";
}

int main() {
  int x{2};
  myFunc(x);
  myFunc(5);
}
lvalue
rvalue

The && syntax works alongside any of the other type concepts we’ve seen. For example, we can have a const int&&, an auto&&, and so on.

Let's see an example with a custom type:

#include <iostream>
#include <vector>

struct Resource {};
struct BigStruct {
  std::vector<Resource>* Resources;
};

void myFunc(BigStruct& x) {
  std::cout << "lvalue\n";
}

void myFunc(BigStruct&& x) {
  std::cout << "rvalue\n";
}

int main() {
  BigStruct Example{BigStruct{}};
  myFunc(Example);

  myFunc(BigStruct{});
}
lvalue
rvalue

lvalue and rvalue references are the key mechanisms that allow us to differentiate between the copy and movement constructors.

  • The copy constructor receives an lvalue reference (eg MyType& Source). Lvalues are assumed to continue to exist after our function ends, so our function needs to respect it. We need to copy (preserve) its resources.
  • The move constructor receives an rvalue reference (eg MyType&& Source). Rvalues are assumed to be deleted right after our function ends, so our function doesn’t need to respect it. We can move (steal) its resources.

What std::move really does

With an understanding of lvalues and rvalues under our belt, we can go a little deeper on what std::move actually does. From our earlier discussion, it’s perhaps not surprising to find that std::move doesn’t move anything.

Rather, it is equivalent to static casting its argument to an rvalue reference. The following two lines are identical:

BigStruct Example{BigStruct{}};

std::move(Example);
static_cast<BigStruct&&>(Example);

The effect of this is that the reference to our object that is returned by std::move it may (but not necessarily) change which version of an overloaded function is invoked when passed that reference.

The following code sample is identical to the previous one, with one difference: on the highlighted line, we’ve wrapped our Example lvalue with a call to std::move before passing it to myFunc. In the previous code, Example was passed to the lvalue overload of myFunc, but by casting it to an rvalue, it's now going to be passed to the rvalue overload of myFunc:

#include <iostream>
#include <utility>
#include <vector>

struct Resource {};
struct BigStruct {
  std::vector<Resource>* Resources;
};

void myFunc(BigStruct& x) {
  std::cout << "lvalue\n";
}

void myFunc(BigStruct&& x) {
  std::cout << "rvalue\n";
}

int main() {
  BigStruct Example{BigStruct{}};
  myFunc(std::move(Example));

  myFunc(BigStruct{});
}
rvalue
rvalue

Creating the Movement Constructor

Let's pick up from an example close to where we left off in our copy semantics lesson, where we have a Character class that implements a copy constructor:

#include <iostream>
#include <memory>

struct Sword {};

class Character {
 public:
  Character()
      : Weapon{std::make_unique<Sword>()} {};

  Character(const Character& Source)
      : Weapon{std::make_unique<Sword>(
            *Source.Weapon)} {
    std::cout << "\nCopying!";
  }

  std::unique_ptr<Sword> Weapon;
};

int main() {
  Character A{};

  std::cout << "A's Weapon: " << A.Weapon.get();
  Character B{std::move(A)};
  std::cout << "\nA's Weapon: "
            << A.Weapon.get();
  std::cout << "\nB's Weapon: "
            << B.Weapon.get();
}

Within our main function, we cast A to an rvalue reference, and pass it to our Character constructor. We haven’t implemented move semantics yet, so we trigger the copy semantics instead. As a result, to give B its Sword, we create a copy of A's sword :

A's Weapon: 0x1391eb0
Copying!
A's Weapon: 0x1391eb0
B's Weapon: 0x13922e0

Let's implement the move constructor:

#include <iostream>
#include <memory>

struct Sword {};

class Character {
 public:
  Character()
      : Weapon{std::make_unique<Sword>()} {};

  Character(const Character& Source)
      : Weapon{std::make_unique<Sword>(
            *Source.Weapon)} {
    std::cout << "\nCopying!";
  }

  Character(Character&& Source)
      : Weapon{std::move(Source.Weapon)} {
    std::cout << "\nMoving!";
  }

  std::unique_ptr<Sword> Weapon;
};

int main() {
  Character A{};

  std::cout << "A's Weapon: " << A.Weapon.get();
  Character B{std::move(A)};
  std::cout << "\nA's Weapon: "
            << A.Weapon.get();
  std::cout << "\nB's Weapon: "
            << B.Weapon.get();
}

We've added only the highlighted lines - no other code has changed. But, now that our class has defined move semantics, B can just take A's sword, avoiding the expensive copy operation:

A's Weapon: 0xb63eb0
Moving!
A's Weapon: 0
B's Weapon: 0xb63eb0

The Movement Assignment Operator

As we covered in the previous lesson with the copy assignment operator, we also generally want to implement the movement assignment operator. We've done that below, with the remaining class code unchanged:

#include <iostream>
#include <memory>

struct Sword {};

class Character {
 public:
  // ...
  Character& operator=(Character&& Source) {
    std::cout << "\nMoving by assignment!";
    Weapon = std::move(Source.Weapon);
    return *this;
  }

  std::unique_ptr<Sword> Weapon;
};

int main() {
  Character A{};
  std::cout << "A's Weapon: " << A.Weapon.get();
  Character B{};
  B = std::move(A);
  std::cout << "\nA's Weapon: "
            << A.Weapon.get();
  std::cout << "\nB's Weapon: "
            << B.Weapon.get();
}
A's Weapon: 0000026A8D586C10
Moving by assignment!
A's Weapon: 0000000000000000
B's Weapon: 0000026A8D586C10

The following example implements all four movement and copy semantic functions, so we can see the differences:

class Character {
 public:
  Character()
      : Weapon{std::make_unique<Sword>()} {};

  // Copy Constructor
  Character(const Character& Source)
      : Weapon{std::make_unique<Sword>(
            *Source.Weapon)} {}

  // Move Constructor
  Character(Character&& Source)
      : Weapon{std::move(Source.Weapon)} {}

  // Copy Assignment
  Character& operator=(
      const Character& Source) {
    Weapon =
        std::make_unique<Sword>(*Source.Weapon);
    return *this;
  }

  // Move Assignment
  Character& operator=(Character&& Source) {
    Weapon = std::move(Source.Weapon);
    return *this;
  }

  std::unique_ptr<Sword> Weapon;
};

Moved-from objects should have "indeterminate but valid" state

It’s worth reiterating that when we move an object, what we’re doing is moving its resources to a new object, and and leaving the original in a dilapidated, "moved-from" state.

We still need to be somewhat considerate of the moved-from object. After all, it still exists, and that means it can still cause issues if we're not careful. As such, the goal for moved-from objects is generally described as leaving them in a "indeterminate but valid" state.

The main factor that makes validity important is the fact that our old object is going to be destroyed at some point in the future. So, it's going to call it's destructor. If the moved-from state is not mindful of that, we can run into issues.

Consider the following example:

struct MyStruct {
  ~MyStruct() { delete Resources; }
  MyStruct(MyStruct&& Source) {
    Resources = Source.Resources;
  }
  Resource* Resources;
};

Our movement constructor is handing over the resources to the new object. However, when our original object is destroyed, it’s going to delete the resources from under the new object, leaving it with a dangling pointer.

And later, when the new object is deleted, it’s going to try to free Resources again, causing a double-free error.

We can update our movement constructor to defuse these issues. Note, calling delete on a nullptr is supported - it just has no effect:

struct MyStruct {
  ~MyStruct() { delete Resources; }
  MyStruct(MyStruct&& Source) {
    Resources = Source.Resources;
    Source.Resources = nullptr;
  }
  Resource* Resources;
};

We’re using raw pointers here to demonstrate the interactions and what can go wrong, but we should be using smart pointers where possible. They prevent a lot of these issues from ever arising, and allow our class to “just work” without needing us to write so much code to manage memory:

struct MyStruct {
  MyStruct(MyStruct&& Source) {
    Resources = std::move(Source.Resources);
  }
  std::unique_ptr<Resource> Resources;
};

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Memory Management
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++ Type Aliases

Learn how to use type aliases, using statements and typedef to simplify or rename complex C++ types.
a53.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved