Move Constructor and Move Assignment Operator in C++

Move Constructor and Move Assignment Operator in C++

Move semantics were introduced in C++11 along with R-value references. It allows us to avoid unnecessary copying of resources from one object to another. Instead of copying, we can now move or transfer the resources between objects, essentially transferring the ownership of resources from one object to another. This feature is especially valuable when working with classes that manage dynamic resources, like dynamically allocated memory. In this tutorial, we will look into move semantics and the use of the new std::move() function.

But before we understand about Move Semantics we need to understand the Copy Semantics.

What is Copy Semantics? #

Prior to C++11, when we assigned one object to another object, it gets copied using copy constructor. For example, if we have a student class, which contains a pointer name as member variable to store the student name and an int age as another member variable to store student age.

class Student {
public:
    int age;
    char* name;

    Student(int age, const char* name) {
        this->age = age;
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
        std::cout << "Student - Constructor \n";
    }

    // Copy constructor
    Student(const Student& other);

    // Copy assignment operator
    Student& operator=(const Student& other);

    // Destructor
    ~Student() {
        delete[] name;
    }
};

In the constructor, it allocates memory on the heap and assigns it to the pointer. Now, as the Student class contains a pointer as a member variable, we need to implement deep copy in the copy constructor. This involves allocating new memory on the heap, copying the content from the passed object to the new memory, and then deleting the allocated memory in the given Student class object, like this:

// Copy Constructor
Student::Student(const Student& other)
{
    age = other.age;
    name = new char[strlen(other.name) + 1];
    strcpy(name, other.name);
    std::cout << "Copy constructor called\n";
}

// Assignment Operator
Student& Student::operator=(const Student& other)
{
    if (this != &other)
    {
        delete[] name;
        age = other.age;
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
    }
    std::cout << "Assignment operator called\n";
    return *this;
}

Now, if we create a vector of Student class objects and add Student objects to it, like this:

std::vector<Student> vecObj;
vecObj.reserve(2);

vecObj.emplace_back(11, "Mark");
vecObj.emplace_back(12, "Sanjay");
vecObj.emplace_back(13, "Suse");
vecObj.emplace_back(14, "John");
vecObj.emplace_back(15, "Parv");

We initially reserved the vector size to 2 and then added more elements to it. Therefore, it is possible that resizing occurs internally in the vector. During resizing, the vector allocates a larger chunk of memory and copies all existing objects into this new memory. So, Student objects will be copied within the vector from one location to another. Therefore, the output of the above code can be:

Student - Constructor 
Student - Constructor 
Student - Constructor 
Copy constructor called
Copy constructor called
Student - Constructor 
Student - Constructor 
Copy constructor called
Copy constructor called
Copy constructor called
Copy constructor called

So, from the output, we can observe that a total of 5 Student class objects were created, but they were copied several times internally due to resizing within the vector. During each copy, new memory was allocated on the heap for the name pointer, contents were copied, and the old object was deleted. This led to the internal resources of Student objects being copied unnecessarily many times. What if we could move those resources internally?

Although the total number of objects will remain the same, unnecessary copying will still occur.

Problem with Copy Semantics #

Before C++11, this was the primary way to manage resources in C++. It is also known as copy semantics, meaning that when we assigned one object to another or returned an object by value, C++ would create a duplicate of the object’s resources.

So, when we create a new object from another object, the copy constructor will be called, like here:

Student first(31, "Simon");

// Copy constructor will be called here
Student second = first;

Output:

Student - Constructor 
Copy constructor called

When we assign an object to an existing object, the assignment operator will be called.

Student first(31, "Simon");
Student second(45, "Raj");

// Assignment Operator will be called here
second = first;

Output:

Student - Constructor 
Student - Constructor 
Assignment operator called

The same will happen if we have a temporary object and we are trying to create a new object from it. Instead of moving resources internally, the copy constructor or assignment operator will copy resources from the temporary object unnecessarily.

This approach works fine for small, simple objects but can lead to performance bottlenecks when dealing with large or complex objects that manage resources such as dynamically allocated memory, file handles, or other costly resources. Just like in the example above. Suppose there are thousands of objects in the vector, and resizing occurs; temporary copies of these thousands of objects will be created, the copy constructor will be called thousands of times, and each time, memory on the heap will be allocated and deleted, which can become a performance bottleneck.

What is Move Semantics? #

Move semantics were introduced to solve this problem of unnecessary copies of temporary objects by Copy Semantics by allowing “move” operations, which transfer resources from one object to another instead of copying them. This prevents unnecessary duplication, speeds up the code, and conserves memory.

We can improve the above program by implementing a Move Constructor and Move Assignment Operator.

But before moving ahead, we need to understand the idea behind the “Move” concept and transferring resources.

The “Move” Concept: Transferring Resources #

The core idea behind move semantics is resource transfer. Rather than deep copying an object’s resources (as we did in the example of the Student class), a move operation steals or transfers the resources from one object to another. This leaves the original (source) object in a safe, “moved-from” state, often by setting its member pointer variabless to nullptr to prevent double deletion.

For example, in our Student class, move semantics allow us to efficiently transfer the memory allocated on the heap and assigned to the name pointer from one Student object to another without creating a costly copy.

Think of it like this: we have a Student object with a member variable pointer name, which points to memory allocated on the heap.

mindmap
  root((Memory
  On Heap
  #9999))
    first.name

Now, if we copy this object, we need to perform a deep copy, which requires allocating new memory on the heap for the name member variable in the new object and then copying the data into it. But what if we make the name pointer in the new Student object point to the same memory to which the name pointer of the first Student object is pointing?

mindmap
  root((Memory
  On Heap
  #9999))
    first.name
    second.name

This way, in both objects, the member variable name will be pointing to the same memory on the heap, similar to a shallow copy. However, we will then set the name pointer to nullptr in the first object.

mindmap
  root((Memory
  On Heap
  #9999))
    second.name
mindmap
  root((NULL))
    first.name

This way, we say that we have moved the internal resources from the first Student object to the second. That is exactly what happens inside the move constructor and move assignment operator. Moving internal resources from the first object to the second will render the first object obsolete, but if the first object is temporary, it’s beneficial to move the resources.

Now, to implement the move concept and transfer ownership, we need to define a special constructor, i.e., the move constructor, and a special assignment operator, i.e., the move assignment operator. Both functions take an r-value reference of the same class object as a parameter. Let’s look at both functions one by one.

Move Constructor T(const T& other) #

It will be invoked when we create a new object from a temporary object. The definition for the Student class’s move constructor is as follows:

// Move constructor
Student::Student(Student&& other) noexcept
: age(other.age),
    name(other.name)  // Transfer ownership
{  
    // Nullify the source's pointer
    other.name = nullptr;  
    std::cout << "Move constructor called\n";
}

Notice the difference between the parameter types of the copy constructor and the move constructor. In the copy constructor, we take an l-value reference as a parameter, whereas in the move constructor, we accept an r-value reference. Now, when we try to create a new object of this Student class with a temporary object, the move constructor will be invoked because only r-value references can refer to temporary values.

Let’s see an example:

Here, we have a Student class object, and we want to create a new Student class object from it. However, we want to transfer the internal resources of the first object to the second object. To achieve this, we want the move constructor of the Student class to be invoked instead of the copy constructor. For this, we can either use the std::move() function or use static_cast like this:

Student first(31, "Simon");

// Move the resources in `first` to `second`
Student second = static_cast<Student &&>(first);

std::cout<< "Second Name : " << second.name << std::endl;
if (first.name == NULL)
{
    std::cout<< "First Name is NULL " << std::endl;
}

Output:

Move constructor called
Second Name : Simon
First Name is NULL 

In this code, we create an r-value reference from an l-value using static_cast<Student &&>. Now, if we try to construct a new Student class object using it, the move constructor will be invoked because we have type-cast it to an r-value reference to simulate the behavior of a temporary object.

mindmap
  root(("Simon"))
    second.name
mindmap
  root((NULL))
    first.name

So now, all the internal resources of the first object are transferred to the second object in the constructor. If you check the name variable of the first object, it will be NULL, and if you check the name variable of the second object, it will have the value "Simon", indicating that the internal memory was transferred.

Using std::move() Function #

What is std::move() function?

The std::move() function was introduced in C++11, and it allows transfer of resources (like memory) from one object to another without copying. It casts an object to an rvalue, enabling efficient resource movement, especially for large objects or containers, optimizing performance by avoiding costly deep copies.

Instead of using static_cast<Student &&>, we can also use the std::move() function.

// Move the resources in `first` to `second`
Student second = std::move(first);

The result will be the same as above: the move constructor will be called, and all resources from the first object will be moved to the second object.

Basically, std::move(a) is equivalent to static_cast<ABC&&>(a) where a is of type ABC. The std::move function is used to indicate that an object may be “moved from,” allowing the efficient transfer of resources from one object to another, as demonstrated in the code above.

Just as the move constructor complements the copy constructor, we also have a move assignment operator that complements the copy assignment operator.

Move Assignment Operator (operator=(T&& other)) #

The move assignment operator helps us transfer resources from one object to another, leaving the original object in a safe, “moved-from” state.

Let’s implement a move assignment operator for our Student class:

// Move assignment operator
Student& operator=(Student&& other) noexcept
{
    if (this != &other) 
    {
        // Free existing resource
        delete[] name;  

        // Transfer ownership of resources
        age = other.age;
        name = other.name;
        
        // Nullify the source's pointer
        other.name = nullptr;  
    }
    std::cout << "Move assignment operator called\n";
    return *this;
}

This is how we can implement a move constructor. It’s like a shallow copy of resources from the first object to the second object, followed by the deletion of resources in the first object. So, basically, resources were moved from the first object to the second object. The steps we followed here are:

  • First, ensure that the move constructor has not been invoked with a pointer to the same object. If not, then:
    • Transfer the name pointer and age from the other object to the calling object’s name and age member variables, like a shallow copy.
    • Set other.name to nullptr to mark it as “moved-from,” preventing double-deletion when the other object goes out of scope.
    • Return the current object as a reference.

Now let’s look at a simple example. Suppose we have two Student class objects, i.e. object first and second,

Student first(31, "Simon");
Student second(42, "Max");

and we want to move all the resources of object second to object first. If we simply use the assignment operator like this,

// Calls the assignment operator
// and deep copies the resources of second to first object
first = second;

All the resources of second will be copied to first, and the assignment operator will destroy the internal resources of first. However, this involves an extra copy, as second and first will now have the same content, whereas we wanted to transfer the contents of second to first directly. For this, we need to call the move assignment operator.

The move assignment operator accepts an r-value reference, so it is designed to accept a temporary object and move its resources to the calling object. In this case, we can use std::move() as in the previous example to make second an r-value, like this:

// Move the resources in `second` to `first`
first = std::move(second);

Here, std::move() will return an r-value reference, and the move assignment operator will be called. This way, no copying will take place. All the internal resources of object first will be destroyed, and the internal resources of object second will be moved to object first. Essentially, the name pointer of object first, which was pointing to memory on the heap, will be deleted, and the name pointer will start pointing to the memory to which object second’s name pointer is pointing. Now name pointer of both the objects first & second are pointing to same memory location on heap. Then we set the object second’s name pointer to nullptr. This way, all internal resources of object second are moved to object first.

mindmap
  root(("Max"))
    first.name
mindmap
  root((NULL))
    second.name

Output of the above code will be,

Move assignment operator called
First Name : Max
Second Name is NULL 

Now, if we print the contents of the first and second objects, the name in object first will be "Max", and the name pointer in object second will be NULL because the resources of object second were moved to object first.

Move Semantics with STL Containers #

Now, both the move assignment operator and the move constructor are designed to work with temporary objects, because temporary objects are r-values. When we try to create a reference to a temporary object, it can be an r-value reference. In such cases, either the move constructor or the move assignment operator will be called.

In the example above, we deliberately created an r-value reference from an l-value using std::move(). However, when we work with STL containers, these containers internally create a lot of temporary objects and copy them around. Let’s look at an example:

std::vector<Student> vecObj;
vecObj.reserve(2);

vecObj.emplace_back(11, "Mark");
vecObj.emplace_back(12, "Sanjay");
vecObj.emplace_back(13, "Suse");
vecObj.emplace_back(14, "John");
vecObj.emplace_back(15, "Parv");

In this example, where we have a vector of Student objects and initially reserve its size to 2, suppose we then add 5 elements to it. When we add the third element, internal resizing of the vector will occur, meaning that on adding the 3rd, 4th, and 5th elements, the vector will allocate a larger chunk of memory and attempt to move the Student objects from one memory location to the larger memory location.

If our Student class has implemented a move constructor or move assignment operator, the objects will be moved instead of copied during resizing. So output will be,

Constructor called
Constructor called
Constructor called
Move constructor called
Move constructor called
Constructor called
Constructor called
Move constructor called
Move constructor called
Move constructor called
Move constructor called

A total of 5 Student objects were created and then destroyed as resizing happens. It is clear from output that no copy constructor was called during resizing; instead, objects were moved using the move constructor. This happened because we implemented the move constructor in the Student class, and now STL containers have been upgraded to use move semantics to move objects internally from one memory area to another instead of creating temporary copies. But this can be possible only if the class whose objects we are storing in container has implemented the move constructor.

This greatly improves performance because objects were not copied and copy constructors were not called. Instead, objects were moved, and the move constructor was invoked to transfer resources.

Summary #

Move semantics provide a powerful optimization technique in C++. By implementing move constructors and move assignment operators, we can transfer resources without duplicating them, making our code more efficient and performant. In cases where classes manage resources like dynamic memory, using move semantics can lead to substantial performance gains

Author: Varun

A Software Developer with 20 Years of Experience in C/C++