Move Semantics

Learn how we can improve the performance of our types using move constructors, move assignment operators and std::move()
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
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated

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)

Resources and Subresources

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

Why We Need Move Semantics

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 Move Constructor

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:

  • An additional & is appended to the parameter type, as in Resource&& instead of Resource&. We cover what this && syntax means in the next lesson
  • Given the move constructor is eventually going to modify the source object (specifically, to steal its subresources), we don’t mark the parameter as const

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 semantics
  • B 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)}; }

Implementing Move Semantics

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:

  • The copy constructor is for scenarios where we still want to use the original object. The copy constructor leaves the original’s subresources in place, so it can continue operating as normal.
  • The move constructor is for scenarios where we no longer need the original object. It moves the original object’s subresources to the new object. This is faster than copying them, but the original object may not be usable afterward, as it no longer has its subresources.

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

The Moved-From State

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;
};

The Move Assignment Operator

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

Moving an object to itself

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

Copy and Move Semantic Interactions

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

Summary

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:

  • The difference between copy semantics and move semantics, and the situations where each is appropriate.
  • How subresources within objects make deep copying expensive and the role of move semantics in mitigating this cost.
  • The implementation of the move constructor, which allows an object to take ownership of another object's resources.
  • The use of std::move() to explicitly indicate that an object's resources can be moved, rather than copied.
  • The concept of the moved-from state, and how objects in this state are typically left in a valid but unspecified state.
  • How to fully implement move semantics in classes, through both the move constructor and move assignment operator
  • The importance of handling self-assignment safely in move operations.
  • The interaction between copy and move semantics, and how the compiler chooses between them based on the context.

Was this lesson useful?

Next Lesson

Value Categories (L-Values and R-Values)

A straightforward guide to l-values and r-values, aimed at helping you understand the fundamentals
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
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
A computer programmer
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:

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

Value Categories (L-Values and R-Values)

A straightforward guide to l-values and r-values, aimed at helping you understand the fundamentals
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved