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 andage
from theother
object to the calling object’sname
andage
member variables, like a shallow copy. - Set
other.name
tonullptr
to mark it as “moved-from,” preventing double-deletion when theother
object goes out of scope. - Return the current object as a reference.
- Transfer the
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