std::move
and rvaluesstd::move
really does.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.
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.
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:
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
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..
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.
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.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.std::move
really doesWith 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
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
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;
};
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;
};
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.