std::move()
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 first understand why we need this feature at all, so let’s introduce some of the problems it’s designed to solve.
Often, the objects we create will need to store other objects within them. These sub-objects can often be containers such as std::vector
objects, which themselves may contain many thousands of their own sub-objects.
For example, if we’re working with databases, we might have objects that are storing thousands of other objects, representing entries in that database.
If we were making a video game, we might have an object representing an entire level, comprised of thousands of sub-objects of various types (enemies, environment art, audio, and so on)
In this lesson, we’ll represent subresources like this using a basic Subresource
type, which we can imagine being expensive to copy. Below, we create two Subresource
objects - one from the default constructor, and one from the copy constructor:
#include <iostream>
struct Subresource {
// Default constructor
Subresource(){
std::cout << "Creating subresource\n";
};
// Copy constructor
Subresource(const Subresource& Source) {
std::cout
<< "Copying subresource (expensive!)\n";
}
};
int main() {
std::cout << "Subresource A:\n";
Subresource SubA;
std::cout << "\nSubresource B:\n";
Subresource SubB{SubA};
}
Subresource A:
Creating subresource
Subresource B:
Copying subresource (expensive!)
Given that a subresource is expensive to copy, it follows that if an object contains these subresources, it will also be expensive to deeply copy. Remember, deep copying refers to copying an object, in addition to copying every object within it.
Below, we introduce a Resource
type to represent this idea. It has a default constructor that creates a new Subresource
, and a copy constructor that copies the other object’s Subresource
:
#include <iostream>
struct Subresource {/*...*/};
struct Resource {
// Default constructor
Resource()
: Sub{std::make_unique<Subresource>()} {
std::cout << "Creating resource\n";
}
// Copy constructor
Resource(const Resource& Source)
: Sub{std::make_unique<Subresource>(
*Source.Sub)} {
std::cout << "Copying resource\n";
}
std::unique_ptr<Subresource> Sub;
};
int main() {
std::cout << "Resource A:\n";
Resource A;
std::cout << "\nResource B:\n";
Resource B{A};
}
Resource A:
Creating subresource
Creating resource
Resource B:
Copying subresource (expensive!)
Copying resource
Naturally, we want to avoid causing expensive actions to occur unnecessarily, as they can degrade performance. Therefore, when copying an object is an expensive action, we naturally want to avoid copying things unnecessarily.
Below, we create a Resource
, and then move it into a std::vector
. With our current implementation, moving our Resource
involves copying it, therefore incurring the performance cost:
#include <iostream>
#include <vector>
struct Subresource {/*...*/};
struct Resource {
// Default constructor
Resource()
: Sub{std::make_unique<Subresource>()} {
std::cout << "Creating resource\n";
}
// Copy constructor
Resource(const Resource& Source)
: Sub{std::make_unique<Subresource>(
*Source.Sub)} {
std::cout << "Copying resource\n";
}
std::unique_ptr<Subresource> Sub;
};
int main() {
std::cout << "Creating resource:\n";
Resource A;
std::vector<Resource> Resources;
std::cout << "\nMoving it into the vector:\n";
Resources.push_back(A);
}
Creating resource:
Creating subresource
Creating resource
Moving it into the vector:
Copying subresource (expensive!)
Copying resource
The basic idea that helps us solve this situation involves introducing an alternative constructor. It works similiarly to the copy constructor but, instead of copying all the subresources to the new object, we just have the new object take control of the existing subresources.
In other words, rather than deeply copying the entire object, we perform a shallow copy, and then transfer ownership of the subresources to this new object. This process is referred to as moving and, to support it, our type would need to implement move semantics.
Let’s take a look at an example type that includes the move constructor: For now, this constructor isn’t doing anything different to the copy constructor, but we’ll add that complexity later in the lesson:
#include <iostream>
struct Resource {
// Default constructor
Resource() {}
// Copy constructor
Resource(const Resource& Source) {
std::cout << "Copying resource\n";
}
// Move constructor
Resource(Resource&& Source) {
std::cout << "Moving resource\n";
}
};
Compared to the copy constructor, we should note two differences in how we define a move constructor:
&
is appended to the parameter type, as in Resource&&
instead of Resource&
. We cover what this &&
syntax means in the next lessonconst
std::move()
When we have implemented both copy and move semantics, we can then decide on a case-by-case basis whether it is more appropriate to copy or move an object.
In situations where we want to use our move semantics over our copy semantics, we can wrap the argument with the std::move()
function. Below, we create three Resource
objects:
Original
is created using the default constructor.A
is created by copying Original
using our type’s copy semanticsB
is created by moving from Original
by wrapping the argument in std::move()
, signalling to the compiler it’s safe to use our type’s move semantics in this scenario:#include <iostream>
struct Resource {/*...*/};
int main() {
std::cout << "Original Resource:\n";
Resource Original;
std::cout << "\nResource A:\n";
Resource A{Original};
std::cout << "\nResource B:\n";
Resource B{std::move(Original)};
}
Getting back to our original goal, we can now represent it as wanting the flexibility of two different ways to create copies of our objects:
Let’s go back to our original example where our Resource
was managing a Subresource
, and implement our move constructor to implement this behaviour. How exactly move semantics should be implemented entirely depends on the nature of the resource and the nature of its subresources.
In this example, we have a single subresource - a std::unique_ptr
. Standard library unique pointers have also implemented move semantics elegantly, allowing us to transfer ownership using std::move()
:
#include <iostream>
struct Subresource {/*...*/};
struct Resource {
// Default constructor
Resource()
: Sub{std::make_unique<Subresource>()} {
std::cout << "Creating resource\n";
}
// Copy constructor
Resource(const Resource& Source)
: Sub{std::make_unique<Subresource>(
*Source.Sub)} {
std::cout << "Copying resource\n";
}
// Move constructor
Resource(Resource&& Source)
: Sub{std::move(Source.Sub)} {
std::cout << "Moving resource\n";
}
std::unique_ptr<Subresource> Sub;
};
We covered std::unique_ptr
in more detail earlier in the chapter:
We can now choose on a situation-by-situation basis whether we want to copy our Resource
objects and take the performance hit, or wrap the argument in std::move()
indicating it’s safe to just harvest the original’s subresources:
#include <iostream>
struct Subresource {/*...*/};
struct Resource {/*...*/};
int main() {
std::cout << "Original Resource:\n";
Resource Original;
std::cout << "\nCopying Original:\n";
Resource A{Original};
if (Original.Sub.get()) {
std::cout
<< "Original still has its subresource\n";
}
std::cout << "\nMoving Original:\n";
Resource B{std::move(Original)};
if (!Original.Sub.get()) {
std::cout << "Original no longer"
" has its subresource";
}
}
Original Resource:
Creating subresource
Creating resource
Copying Original:
Copying subresource (expensive!)
Copying resource
Original still has its subresource
Moving Original:
Moving resource
Original no longer has its subresource
When our move semantics steal the resources from our original object for performance reasons, the original object may no longer be usable. Without its subresources, it can be in a dilapidated state, sometimes called the moved from state.
To tell the compiler we’re no longer interested in the object, and it’s safe to leave it in this state, we wrap it in std::move()
Most compilers can detect when we’re trying to use an object in this state and issue us with warnings:
int main() {
Resource A;
Resource B{std::move(A)};
A.SomeFunction();
}
Warning C26800 Use of a moved from object: 'A'
We also need to be mindful that our moved-from objects still exist until they are freed from memory. This means that even if we don’t directly use the object again, it could still have a destructor that is going to be called at some point in the future.
If we’re not mindful of our move semantics when writing that destructor, it can cause problems. Consider the following example:
struct Resource {
~Resource() { delete Sub; }
Resource(Resource&& Source)
: Sub{Source.Sub} {
// ...
}
Subresource* Sub;
};
Our movement constructor is taking control of the subresource, which is in a memory address we’ve called Sub
. However, when our moved-from object is eventually destroyed, it’s going to delete Sub
. So, the destructor of our moved-from object is going to damage our moved-to object, leaving it with a dangling pointer.
And later, when the moved-to object is deleted, it’s going to try to delete Sub;
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 Resource {
~Resource() { delete Sub; }
Resource(Resource&& Source)
: Sub{Source.Sub} {
Source.Sub = nullptr;
}
Subresource* Sub;
};
We’re using raw pointers here to demonstrate the interactions and what can go wrong, but we should be using smart pointers here. They prevent a lot of these issues from ever arising, and allow our type to "just work" without requiring so much manual memory management.
We can delete our destructor and update Sub
to be a std::unique_ptr
:
struct Resource {
~Resource() { delete Sub; }
Resource(Resource&& Source)
: Sub{std::move(Source.Sub)} {
// ...
}
std::unique_ptr<Subresource> Sub;
};
As we covered in the previous lesson with the copy assignment operator, we also generally want to implement the movement assignment operator. This allows us to also optimise the performance of our types when they’re used in expressions such as B = std::move(A)
:
#include <iostream>
struct Subresource {/*...*/};
struct Resource {
// Default constructor
Resource()
: Sub{std::make_unique<Subresource>()} {
std::cout << "Creating resource\n";
}
// Move constructor
Resource(Resource&& Source)
: Sub{std::move(Source.Sub)} {}
// Move assignment
Resource& operator=(Resource&& Source) {
std::cout << "Moving by assignment\n";
Sub = std::move(Source.Sub);
return *this;
}
std::unique_ptr<Subresource> Sub;
};
int main() {
std::cout << "Resource A:\n";
Resource A;
std::cout << "\nResource B:\n";
Resource B;
std::cout << "\nMoving A to B:\n";
B = std::move(A);
if (!A.Sub.get()) {
std::cout << "A no longer"
" has its subresource";
}
}
Resource A:
Creating subresource
Creating resource
Resource B:
Creating subresource
Creating resource
Moving A to B:
Moving by assignment
A no longer has its subresource
When implementing the copy operator in the previous lesson, we pointed out how it is technically valid to attempt to copy an object to itself, using an expression like A = A
.
This consideration applies when writing a move assignment operator, too. An expression like A = std::move(A)
is valid, so our move assignment operator should ensure its logic still holds up when both objects are the same.
We can test for this scenario by comparing the address of the source object (that is, the function parameter) to the this
pointer:
#include <iostream>
struct Resource {
Resource() = default;
Resource& operator=(Resource&& Source) {
if (&Source == this) {
std::cout << "Same object - skipping";
return *this;
}
// ...
return *this;
}
};
int main() {
Resource A;
A = std::move(A);
}
Same object - skipping
Move semantics are simply a performance optimisation. Even if we don’t implement move constructors and assignment operators, the compiler can still fulfill move requests by using our type’s copy semantics. For example, the copy constructor can stand in for the movement constructor:
#include <iostream>
struct Resource {
Resource() = default;
// Copy constructor
Resource(const Resource& Source) {
std::cout << "Copying resource\n";
}
};
int main() {
Resource Original;
Resource Moved{std::move(Original)};
}
Copying resource
In the previous example, we’re indicating the Original
object is safe to be moved-from, that is, we don’t really care about it. However, it’s still reasonable for the copy constructor to step in here given we don’t have a move constructor. The copy constructor can do the job - it may just run slightly slower than a movement constructor could.
The opposite is not true. When we copy something, we’re stating we want the integrity of the original object to be maintained. A movement constructor is not intended for that so the compiler will not use it for copying actions.
As a result, if we try to copy something, and the type doesn’t have the appropriate copy constructor or =
operator, the compiler will throw an error rather than using any available move semantics:
#include <iostream>
struct Resource {
Resource() = default;
// Move constructor
Resource(Resource&& Source) {
std::cout << "Moving resource\n";
}
};
int main() {
Resource Original;
Resource Copied{Original};
}
error: 'Resource::Resource(const Resource &)': attempting to reference a deleted function
In this lesson, we've explored the concept of move semantics, learning how to efficiently transfer resources between objects to improve performance. The key points we learned include:
std::move()
to explicitly indicate that an object's resources can be moved, rather than copied.Learn how we can improve the performance of our types using move constructors, move assignment operators and std::move()
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.