How to Move Semantics in C++

Abdul Mateen Feb 02, 2024
  1. C++ Object Creation Preliminaries
  2. Move Semantics in C++
How to Move Semantics in C++

In this tutorial, we will discuss move semantics in C++:

  • We will discuss related concepts of deep copy & shallow copy.
  • We will quickly discuss the idea of lvalue and rvalue.
  • We will try to understand move semantics with examples.

Note: If you are confident with shallow and deep copy concepts, you can directly jump to the Move Semantics section.

C++ Object Creation Preliminaries

Let’s quickly understand the mechanism for the creation of an object’s copy using the following trivial example:

class T {
  int x;

 public:
  T(int x = 1) { this->x = x; }
  int getX() const { return x; }
  void setX(int x) { this->x = x; }
};
int main() {
  T o1;
  T o2(o1);
  cout << o1.getX() << '\t' << o2.getX() << '\n';
  o2.setX(5);
  cout << o1.getX() << '\t' << o2.getX() << '\n';
  return 0;
}

In this simple example, class T has only one data member: x. We have written one parameterized constructor with a default value and two factory methods for x.

In main(), we have created object o1 using the available constructor. In the second line, we made another object, o2, a copy of o1.

This object will be created through a copy constructor that exists by default in every class. This default constructor does a member-wise copy of the data members.

In line 11, we are printing data members of both objects, while line 4 changes the value of data member x of o2. Finally, we are displaying data members again to see the effect of modification on both objects.

Here is the output of this code:

1	1
1	5

In the first output line, both data members have the value 1 because the first object is created using the parametrized constructor with the default value 1. As the second object is a copy of o1, both values are the same.

However, later, o2 calls the setter function to modify the value of its data member. The second line of output shows that changing the value of data members of o2 does not affect the value of the data members of o1.

Shallow Copy

Unfortunately, the above copy mechanism doesn’t work in case the data members are pointers pointing to dynamic memory (created on the heap). In this scenario, the copier object points to the same dynamic memory location created by the previous object; therefore, the copier object is said to have a shallow copy of another object.

The solution works fine if objects or data members are read-only; otherwise, it can create serious issues. Let’s first understand the shallow copy concept by the following code example:

class T {
  int *x, size;

 public:
  T(int s = 5) {
    size = s;
    x = new int[size];
    x[0] = x[1] = x[2] = x[3] = x[4] = 1;
  }
  void set(int index, int val) { this->x[index] = val; }
  void show() const {
    for (int i = 0; i < size; i++) cout << x[i] << ' ';
    cout << '\n';
  }
};
int main() {
  T o1;
  T o2(o1);
  o1.show();
  o2.show();
  o2.set(2, 5);
  o1.show();
  o2.show();
  return 0;
}

Class T has two data members in this example: pointer x and size. Again, we have a parameterized constructor with a default value.

We have assigned the default parameter value to the data member size inside the constructor body.

Line 7 and 8 in the constructor declare a dynamic array of size length and assigns 1 to all array elements. The set method places the value of val to the index location of the dynamic array.

In main(), we created o1 and o2, where o2 is the copy object. Next, we are printing both objects.

Again, we are modifying the value of the third element of the array in o2 and printing objects.

Here is the output of this code:

1 1 1 1 1
1 1 1 1 1
1 1 5 1 1
1 1 5 1 1

Now, the output may not be as per your expectations. The first two lines show an exact copy; however, in the third and fourth lines, we have the same elements in both objects, whereas we have modified only one.

This is because the default copy constructor has created a shallow copy by assigning the value of the pointer in the first object (which is an address to the dynamically allocated array) to the pointer of the second object.

Again this shallow copy is fine if we have read-only values; otherwise, the output clearly shows the update hazards.

Deep Copy

Opposite to the shallow copy, in the deep copy, we allocate separate dynamic memory for each object. Then we do a member-wise copy for each element inside the heap.

This is essentially achieved by overloading the copy constructor and the assignment operator. See the coding example, where we are overloading the copy constructor:

T(const T &t) {
  size = t.size;
  x = new int[size];
  for (int i = 0; i < size; i++) x[i] = t.x[i];
}

Please note that in the second line, the address of the new dynamic allocation is assigned to x. In lines 3 and 4, we have copied all the dynamic array elements to create a deep copy.

Note: The above example provides code for copy constructor overloading only. The rest of the code is the same as in the previous example.

Here is the output of the main code written in the previous example after adding the copy constructor:

1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 5 1 1

Lines 3 and 4 show no modification effect in the second object on the first object. This means the change is only inherent to the second object because now 2 is a deep copy of o1.

Now, let’s quickly discuss the concept of lvalue & rvalue.

LValues and RValues

The lvalue or left value refers to the operand on the left of the assignment operator receiving the value. In contrast, the rvalue means the operand on the right side of the assignment operator providing the value.

Therefore, the simple variables can be used as both lvalue and rvalue. Constants or variable constants can be used only as an rvalue.

The expressions are rvalue. For example, we may write a = b + c, but we can’t write b + c = a or we can’t write 2 = a.

Move Semantics in C++

Finally, let’s start the discussion on move semantics. Here, we will discuss this concept concerning the copy constructors.

First of all, see the following calls to the copy constructor for class T:

T o2(o1);
T o3(o1 - o2);

If we deeply analyze the above code, only the first line requires a deep copy, where we can inspect o1 later. Moreover, we can have a statement in the copy constructor to modify o1 contents (e.g., a statement like t.set(2,5)).

Therefore, we say o1 is an lvalue.

However, the expression o1-o2 is not an lvalue; instead, it is an rvalue as it is a unanimous (having no name) object, and we don’t need to access o1-o2 again. Hence, we can say that rvalues represent temporary objects destroyed soon after executing the statement under consideration.

C++0x has introduced a new approach called RValue Reference for reference of the rvalue type. This new approach allows matching the rvalue arguments in function overloading.

We have to write another constructor with an rvalue reference parameter for this.

T(T&& t) {
  x = t.data;
  t.data = nullptr;
}

First, note the new constructor (called move constructor instead of copy constructor), where we have added one more & sign that is a reference for an rvalue.

In line 2, instead of doing a deep copy of the dynamic array, we have just assigned the address of the existing object to the new object. For further safety, we have assigned nullptr to the pointer of the existing object.

The rationale is that we don’t need to access the temporary source object. Therefore, in the move constructor, we move the resource (i.e., the dynamic array) from the existing (temporary) object to the new calling object.

To understand it better, let’s look at another code that overloads the assignment operator:

T& operator=(T t) {
  size = t.size;
  for (int i = 0; i < size; i++) x[i] = t.x[i];
  return *this;
}

We have not used the rvalue reference here. In C++0x, the compiler will check whether the parameter is rvalue or lvalue, and either the move or copy constructors will be called accordingly.

Therefore, if we call the assignment operator o1=o2, the copy constructor will initialize t. However, if we call the assignment operator o3 = o2-o1, the move constructor will initialize t.

The reason is that o2-o1 is an rvalue instead of an lvalue.

Finally, we conclude that the copy constructor can be used to create a deep copy to save the option of keeping the source object safe for further access. The move constructor assigns the dynamic memory of the existing object to the pointer of the calling object because there is no need to access the rvalue object later on.