C++. Move constructor and move operator

Move constructor and move operator. The purpose. Examples. The noexcept keyword. An example of Vector class (dynamic array)

Before exploring this topic, it is recommended that you familiarize yourself with the following topic:


Contents


Search other resources:

1. What special class functions are provided by the compiler by default? Example

When the class is initially declared, the compiler provides 6 special functions by default that can be overridden:

  • default constructor or parameterless constructor. Used to create an instance of a class if no other constructors are implemented in the class;
  • destructor;
  • copy constructor. Uses bitwise copying of instance data. This constructor must be overridden in classes where memory for data is dynamically allocated. If memory in classes is not dynamically allocated (using pointers *), then this constructor does not need to be redefined;
  • copy operator. Uses a bitwise copy of the instance data. This operator must be overridden in classes where memory is dynamically allocated for data. If there is no dynamic memory allocation in the class, then the copy operator need not be overridden;
  • move constructor. It is recommended to override this constructor in classes that can contain (or contain) large data arrays, memory for which is dynamically allocated;
  • move operator. Has the same purpose as move constructor.

Any of the above functions can be overridden in a class with your own implementation.

Example. Let an empty class (stub) named MyClass be given

class MyClass
{
};

For the class MyClass, the compiler generates 6 special functions by default. The signature of these functions is as follows:

  • MyClass() – default constructor. This constructor has no parameters. It is relevant when the class does not have a single constructor that initializes the internal fields (data) of the class;
  • ~MyClass() – destructor. This is a special function called when a class object is destroyed. In this function, it is advisable to write the code for releasing the allocated class resources (freeing memory for data, closing open files, etc.);
  • MyClass(const MyClass&) – copy constructor. This constructor implements what’s called bitwise copying. You can read more about bitwise copying and using the copy constructor here and here;
  • operator(const MyClass&) – copy operator. Implements the so-called bitwise copying. More details about the features of bitwise copying can be found here;
  • MyClass(MyClass&&) – move constructor (described in this topic);
  • operator(MyClass&&) – move operator (described in this topic).

 

2. Move constructor and move operator. The purpose. Features of use. General form

The move constructor and the move operator were added in C++ 11. The main idea behind using these two constructs is to speed up program execution by avoiding copying data during initial initialization and assignment of so-called rvalue references.

It is advisable to declare the move constructor and the move operator in classes containing large data arrays. Nobody bothers to declare a move constructor or a move operator in the class in which there are only a few simple fields of small dimension. However, in this case, the effect of using the move constructor will be negligible (or not at all).

Also, if it becomes necessary to use these structures, it is recommended to add them in pairs (both structures).

If the move constructor is not implemented in the class, then its call is replaced by the copy constructor. If the move operator is not implemented in the class, then its call is replaced by the copy operator.

The general form of declaring a move constructor in a class

ClassName(ClassName&& rObj) noexcept
{
  ...
}

here

  • ClassName – the name of the class and constructor;
  • rObj – reference to a reference to a temporary instance of the class (rvalue – instance), the value of which will be copied to the current instance.

The above general form uses the noexcept keyword. This specifier indicates that our function (the move constructor) does not throw (throw) an exception or crash. The compiler recommends using the word noexcept for the move constructor and the move operator. In the move constructor, no memory operations are performed (memory allocation, memory release, writing data to the allocated memory area, etc.), but a simple assignment of the pointer (s) takes place.

 

3. Figure showing the purpose of a move constructor in a class

Figure 1 shows the case when the class does not implement the move constructor and the copy constructor is called instead. Copying a large data array A with dimension n elements for a class named MyClass is considered. As you can see from the figure, the entire array is copied element by element. If the number of elements in this array is large, this process will take some time.

C++. Calling the copy constructor. Copying the entire array

Figure 1. Actions performed by the copy constructor. Copying the entire array

After adding the move constructor to the MyClass class, the data is no longer copied (Figure 2). This gives a significant gain in the speed of program execution if the number of elements in the obj.A array is significant (for example, n = 1E6).

C++. Move constructor. Assigning (redirecting) a pointer to the source array

Figure 2. Move constructor. Assigning (redirecting) a pointer to the source array. Data is not copied

 

4. Ways of calling the move constructor. References of type rvalue- and lvalue-. Actions performed in the move constructor

This topic covers only the general features of rvalue- and lvalue- references. A detailed overview of lvalues and rvalues is a completely different topic that requires a separate thorough study.

If a move constructor is declared in a class, then it is called in cases when the expression that initializes the value of an instance of this class with the = operator receives another instance, which is a so-called rvalue reference.

Below we consider one of the possible situations of calling the move constructor.

In the most general case, any function that returns an instance of the ClassName class looks like this:

ClassName SomeFunc(parameters)
{
  ...
}

The move constructor is called when the SomeFunc() function is called at the time the class instance is declared.

ClassName obj = SomeFunc(...); // here the move constructor is called

The obj instance is located on the left side of the assignment operator and is an lvalue reference. Such a reference is scoped within curly braces { } and is available after the current expression completes. In other words, you can use the obj instance in the future, for example

obj.SomeInternalMethod(...);

here SomeInternalMethod() is some public method from the ClassName class.

In turn, the SumeFunc() function is placed on the right side of the = operator. The result returned by the function is an instance of the ClassName class. In this case, this instance is a temporary rvalue reference. The scope of this temporary object is determined by the current expression. This temporary instance will not be used after the assignment (Figure 3).

C++. The case where the move constructor is called

Figure 3. The case where the move constructor is called. Scopes for lvalue and rvalue references

If a class implements its own move constructor, such initializations will call that constructor instead of the copy constructor.

If you make a correct assignment from an rvalue to an lvalue reference in the move constructor, you can avoid unnecessary copying of data from the memory area pointed to by the rvalue reference to the area pointed to by the lvalue reference. With an increase in the amount of data in the class (for example, large amounts of information), the effect of using the move constructor will increase.

The following actions are performed in the move constructor:

  • internal pointers should be redirected to external data, which should be copied into the internal fields of the class. In other words, internal pointers receive the values of addresses of memory areas containing data received from outside (see example below).

 

5. Move operator. General form

The purpose of using the move operator is the same as that of the move constructor – to speed up program execution by avoiding direct copying of data when assigning so-called rvalue references, which are used in expressions on the right side of the assignment operator.

If a move operator is declared in a class, then it is called in cases when an instance of a class is obtained in the assignment operator (=), which is the result of a return from another function.

ClassName obj;
obj = SomeFunc(parameters); // here the move operator is called

here SomeFunc() is some function that returns the instance of the class ClassName.

If the class does not implement the move operator, then this operator is replaced by the copy operator.

The general form of declaring a move operator in a class:

ClassName& operator=(ClassName&& obj)
{
  ...
}

here

  • ClassName – the name of the class;
  • obj – object that is an rvalue reference in the calling expression.

The move operator has more sequence of actions than the copy constructor, namely:

  • checking whether an instance is being assigned to itself in cases where a function can somehow return the same instance (see examples below);
  • freeing memory for allocated internal data. The lvalue has already been instantiated and already contains some data;
  • assignment of internal pointers to the addresses of the data to be copied into the current instance.

For more details on the implementation of the move operator, see the example below.

 

6. An example of implementation of the Vector class (dynamic array). Basic set of methods. The move constructor and the move operator in the class

The example demonstrates the declaration and use of the Vector class, which implements a dynamic array of type double*. The class uses a basic set of special class functions and methods for demonstration purposes. These functions ensure the correct functioning of class instances (memory allocation, deallocation, exception handling, etc.). Optionally, you can reprogram this class to a one-dimensional array for the generic type T.

You can also extend the class by adding new methods that operate on an array. For example, you can add methods for reversing an array, concatenating, accessing array elements by index, etc.

 

6.1. Task

Develop a class that is a dynamic array. In the class, form a minimum set of special functions for organizing work with an array.

6.2. Solution
6.2.1. The components of the Vector class

The class contains the following components:

  • A – array of double* type;
  • count – the number of elements in the array
  • Vector(double*, int) – a parameterized constructor that initializes the class data;
  • Vector() – constructor without a parameters;
  • Vector(const Vector&) – copy constructor;
  • Vector(Vector&&) – move constructor;
  • operator=(const Vector&) – copy operator;
  • operator=(Vector&&) – move operator;
  • ~Vector() – destructor;
  • Free() – an internal private-function that frees the data allocated for array A;
  • CopyArray() – an internal private function that copies external data into an internal array;
  • метод Set() – implements copying of an external array to an internal one;
  • метод Print() – prints the array to the screen. Used for testing.

The above list is the basic (minimum) set of functions to ensure the correct functioning of the class. Optionally, this set can be expanded with additional functions.

 

6.2.2. Internal class fields

A dynamic array of elements of type double is declared with double*. The number of elements in the array is count. After entering these variables, the class looks like this

class Vector
{
private:
  double* A; // array of double type
  int count; // the number of elements in the array
};

In our case, it is agreed that the check for the presence of an empty array is carried out based on the value of count. If count> 0, then the array is not empty. In all other cases, the array is considered empty. The control over the filling of the array lies entirely with the count variable.

 

6.2.3. Internal private functions Free(), CopyArray()

In different methods of the class, the program code will be repeated. The basic operations commonly used on an array are:

  • freeing the memory allocated for the array;
  • copying an external array to an internal array A.

Therefore, it is advisable to implement the corresponding internal private-functions Free() and CopyArray() in the class. After entering the functions, the program code of the class is as follows:

class Vector
{
private:
  double* A; // the array of double type
  int count; // the number of elements in the array

  // A function that frees the memory allocated for the array.
  void Free()
  {
    // The presence of elements in the array is controlled by the count variable
    if (count > 0)
    {
      delete[] A;
      count = 0;
    }
  }

  // A function that copies an external array to the current one
  void CopyArray(double* A, int count)
  {
    // 1. Free memory if needed
    Free();

    try
    {
      // 2. Allocate memory for the internal array this->A
      this->A = new double[count];

      // 3. Copy data to internal array
      this->count = count;
      for (int i = 0; i < count; i++)
        this->A[i] = A[i];
    }
    catch (bad_alloc e)
    {
      // 4. If memory is not allocated, then exception catching
      cout << e.what() << endl;
    }
  }
}

The Free() function frees the memory allocated for the array A. This function will be called from other functions in cases when it is necessary to reallocate memory or free memory.

In the Free() function, the fact of the presence of parts in the array is checked by the count variable (count==0). Therefore, in the case of an empty array, you do not need to assign nullptr to the pointer A every time.

 

6.2.4. Constructor with two parameters Vector(double*, int)

When designing classes, a different number of constructors can be used to initialize internal data. The first is the constructor that initializes the array to cover the largest number of internal fields of the class. In our case, the following constructor with two parameters is introduced into the public section:

// Constructor that receives external array A
Vector(double* A, int count)
{
  this->count = 0;
  CopyArray(A, count); // call the copy function
}

The constructor uses the internal CopyArray() function to copy data to the internal array A.

 

6.2.5. Destructor ~Vector()

After declaring a parameterized constructor, a destructor must be declared that calls the internal function Free().

// Destructor
~Vector()
{
  Free();
}

 

6.2.6. Parameterless constructor Vector()

Another constructor that can be used to create an empty array is the parameterless constructor. This constructor delegates its authority to the constructor with two parameters. The constructor is introduced in the public section.

// Parameterless constructor - delegates authority to a constructor with two parameters
Vector() : Vector(nullptr, 0) {   }

This is the only time the program uses nullptr to assign a value to pointer A. In all other cases, you do not need to set pointer A to nullptr, since control over the presence of an empty array is entirely in the variable count.

 

6.2.7. Copy constructor Vector(const Vector&)

Since our class uses dynamic memory allocation for internal data, it is imperative to use the copy constructor to avoid the disadvantages of bitwise copying. More details about this are described here and here.

In our case, the copy constructor code is extremely simple. The CopyArray() function is called to do all the necessary work.

// Copy constructor
Vector(const Vector& obj)
{
  CopyArray(obj.A, obj.count);
}

 

6.2.8. Copy operator operator=(const Vector&)

The copy operator must be implemented in cases where the class uses dynamic memory allocation. In our case, the copy operator code contains a call to the CopyArray() function.

// Copy operator
Vector& operator=(Vector& obj)
{
  CopyArray(obj.A, obj.count);
  return *this;
}

 

6.2.9. Copy constructor Vector(Vector&&)

The transfer constructor code does not perform any memory operations (memory allocation, deallocation, etc.).

// Move constructor
Vector(Vector&& obj) noexcept
{
  // 1. Redirect A to obj.A and change count
  A = obj.A;
  count = obj.count;

  // 2. Zero the number of elements in the original array,
  // this is necessary to avoid unnecessary freeing
  // memory in the destructor, which can throw an exception
  obj.count = 0;
}

In the move constructor, the most important line is

obj.count = 0;

This action is required, because when assigning pointers

A = obj.A;

we get a situation that both pointers (A and obj.A) point to the same memory area. In the case of freeing memory for pointers, the same memory area will be freed twice, and this will result in an exception being thrown. To avoid this, the number of elements in the temporary object obj.count is set to 0. When calling the function Free(), which frees memory, count is checked for a nonzero value; if count == 0, then the memory is not freed, and, therefore, unnecessary (unnecessary) memory will not be freed.

 

6.2.10. Move operator operator=(Vector&&)

Since our class has a dynamic array that can have an arbitrary number of elements, it is recommended to declare a move operator in it.

// Move operator
Vector& operator=(Vector&& obj) noexcept
{
  // 1. Checking if there is an assignment to itself
  if (&obj == this)
    return *this;

  // 2. Free pre-allocated memory for array A
  Free();

  // 3. Redirect pointer A to obj.A and assign a different number of items
  count = obj.count;
  A = obj.A;

  // 4. Zero obj.count to avoid double free
  //    the same memory area
  obj.count = 0;

  // 5. Return the current instance
  return *this;
}

 

6.2.11. Method Set(double*, int). Set a new array

For demonstration purposes, the class implements the Set() method, which makes a copy of the external array, which is an input parameter, into the internal array.

// Set() method - copies external data into an internal array
void Set(double* A, int count)
{
  CopyArray(A, count);
}

It would be wrong to assign like

this->A = A;

since two arrays (external and internal) will point to the same memory location, which in some situations can lead to subtle errors.

 

6.2.12. Method Print(string). Display an array with the given message

To get the current state of the class, the Print() method is introduced.

// Input array
void Print(string msg)
{
  cout << msg << endl;
  if (count > 0)
  {
    // If there are elements in the array
    for (int i = 0; i < count; i++)
      cout << A[i] << " ";
    cout << endl;
  }
  else
  {
    // If the array is empty
    cout << "{ }" << endl;
  }
}

 

6.2.13. General structure of the Vector class

After completing clauses 4.2.2 – 4.2.13, the Vector class in its abbreviated form will be as follows.

// Class Vector - dynamic array of numbers of type double
class Vector
{
private:
  double* A; // array of double type
  int count; // the number of elements in the array

  // The function that frees the array
  void Free()
  {
    ...
  }

  // Function that copies an external array to the current one
  void CopyArray(double* A, int count)
  {
    ...
  }

public:
  // Constructor that receives array A
  Vector(double* A, int count)
  {
    ...
  }

  // Constructor without parameters
  Vector() : Vector(nullptr, 0) {   }

  // Copy constructor
  Vector(const Vector& obj)
  {
    ...
  }

  // Copy operator
  Vector& operator=(Vector& obj)
  {
    ...
  }

  // Move constructor
  Vector(Vector&& obj) noexcept
  {
    ...
  }

  // Move operator
  Vector& operator=(Vector&& obj) noexcept
  {
    ...
  }

  // Destructor
  ~Vector()
  {
    ...
  }

  // Set() method - copies external data into the internal array
  void Set(double* A, int count)
  {
    ...
  }

  // Display the array
  void Print(string msg)
  {
    ...
  }
};

 

6.2.14. Method GetV(). Get a random array

To demonstrate how to call the move constructor and the move operator outside the Vector class, the GetV() function is introduced, which forms an arbitrary array and returns an instance of the Vector type.

// External function required for call demonstration
// move constructor and move operator.
// The function returns an instance of the Vector type.
Vector GetV()
{
  double A[] = { 2.3, 4.5, 1.7, 2.8 };
  Vector AV(A, 4);
  return AV;
}

 

6.2.15. Function main()

The test of the Vector class is done in the main() function.

void main()
{
  // 1. Construct an instance using a regular constructor
  double AD[] = { 2.8, 1.3, -0.9, 12.3 };
  Vector v1(AD, 4);
  v1.Print("v1");

  // 2. Calling the move constructor
  Vector v2 = GetV();
  v2.Print("v2");

  // 3. Calling the move operator
  Vector v3;
  v3 = GetV();
  v3.Print("v3");

  // 4. Method Set() demonstration
  Vector v4;
  v4.Set(AD, 4);
  v4.Print("v4");
}

 

6.2.16. General structure of the program

In the most general case, the program structure has the form

#include <iostream>
using namespace std;

// Vector class - a dynamic array of doubles
class Vector
{
  ...
};

// The function returns an instance of the Vector type.
Vector GetV()
{
  ...
}

void main()
{
  ...
}

 

6.2.17. Run the program

After combining all the above code snippets, running the program will produce the following result

v1
2.8 1.3 -0.9 12.3
v2
2.3 4.5 1.7 2.8
v3
2.3 4.5 1.7 2.8
v4
2.8 1.3 -0.9 12.3

 


Related topics