Introduction to Move constructors and Move assignment constructors

1. General

This paper will focus on the "Move" semantic related mobile construction and mobile assignment constructor, and also give a comparison with the "Copy" semantic Copy construction and Copy assignment constructor.

2. Copy construction and copy assignment constructor

Before understanding the move constructor and the move assignment constructor, let's take a look at the copy constructor and the copy assignment constructor.

  1. Copy constructor: initialize a class object with an object of the same class by copying;
  2. Copy assignment constructor: assign a class object to an existing class object of the same class by copying.

If the copy constructor and copy assignment constructor are not explicitly given in the code and will be used again, the compiler will generate a set of default copy constructor and copy assignment constructor (shallow copy version). If the class handles dynamic memory allocation, we should rewrite our own "deep copy" version.
Recall scheme 2 in Chapter 5 of our previous article Move semantics and Smart Pointers (illustrated by an example). At that time, it was said that there was a cost to adopt copy construction and copy assignment constructor. Let's see what the cost is here.

#include <iostream>

template<typename T>
struct AutoPtr3
{
    AutoPtr3(T* ptr = nullptr)
        : ptr(ptr)
    {
    }

    ~AutoPtr3()
    {
        if(this->ptr != nullptr)
        {
            delete this->ptr;
            this->ptr = nullptr;
        }
    }

    AutoPtr3(const AutoPtr3& ptr3) // deep copy
    {
        this->ptr = new T;
        *this->ptr = *ptr3.ptr;
    }

    AutoPtr3& operator=(const AutoPtr3& ptr3) // deep copy
    {
        if(this == &ptr3)
        {
            return *this;
        }

        delete this->ptr;
        this->ptr = new T;
        *this->ptr = *ptr3.ptr;
        return *this;
    }

    T& operator*() const
    {
        return *this->ptr;
    }

    T* operator->() const
    {
        return this->ptr;
    }

    bool isNull() const
    {
        return this->ptr == nullptr;
    }

private:
    T* ptr;
};

struct Resource
{
    Resource()
    {
        std::cout << "Resource acquired" << std::endl;
    }

    ~Resource()
    {
        std::cout << "Resource destroy" << std::endl;
    }
};

int main()
{
    AutoPtr3<Resource> res1{new Resource()}; // local variable, constructor
    AutoPtr3<Resource> res2;
    res2 = res1; // copy assignment

    return 0;
}

The output of the program is as follows:

Resource acquired
Resource acquired
Resource destroy
Resource destroy

At this time, the program runs according to the following steps:

  1. The first Resource acquired occurs in the local variable construction: autoptr3 < resource > res1 {new resource()};
  2. The second Resource acquired occurs in the copy assignment structure: res2 = res1;
  3. The first Resource destroy occurs when main() is about to end after the copy assignment construction, and res2 is destructed first;
  4. The second Resource destroy occurs when main() is about to end after the copy assignment construction, and res1 is destructed.

It can be seen from the above that there is at least one local variable construction + copy construction / copy assignment construction of the second variable using copy semantics.

3. Move construction and move assignment constructor

The Copy constructor and Copy assignment constructor Copy an object to another object of the same type, while the Move constructor and Move assignment constructor only transfer the ownership of one object to another object of the same type. Obviously, the cost of "Move" semantics is less than that of "Copy".
It should be noted here that functions with "Copy" semantics adopt const lvalue references, while functions with "Move" semantics adopt non const rvalues (lvalues and rvalues will be introduced in the next article analysis of lvalues, rvalues, lvalues and rvalues). The codes are as follows:

#include <iostream>

template<typename T>
struct AutoPtr4
{
    AutoPtr4(T* ptr = nullptr)
        : ptr(ptr)
    {
    }

    ~AutoPtr4()
    {
        if(this->ptr != nullptr)
        {
            delete this->ptr;
            this->ptr = nullptr;
        }
    }

    AutoPtr4(const AutoPtr4& ptr4) = delete; // disable copying

    AutoPtr4(AutoPtr4&& ptr4) noexcept // move constructor
        : ptr(ptr4)
    {
        ptr4.ptr = nullptr;
    }

    AutoPtr4& operator=(const AutoPtr4& ptr4) = delete; // disable copy assignment

    AutoPtr4& operator=(AutoPtr4&& ptr4) noexcept // move assignment
    {
        if(this == &ptr4)
        {
            return *this;
        }

        delete this->ptr;
        this->ptr = ptr4.ptr;
        ptr4.ptr = nullptr;
        return *this;
    }

    T& operator*() const
    {
        return *this->ptr;
    }

    T* operator->() const
    {
        return this->ptr;
    }

    bool isNull() const
    {
        return this->ptr == nullptr;
    }

private:
    T* ptr;
};

struct Resource
{
    Resource()
    {
        std::cout << "Resource acquired" << std::endl;
    }

    ~Resource()
    {
        std::cout << "Resource destroy" << std::endl;
    }
};

int main()
{
    AutoPtr4<Resource> res;
    res = new Resource(); // local variable construct + move assignment

    return 0;
}

The output of the program is as follows:

Resource acquired
Resource destroy

The program runs as follows:

  1. Resource acquired is a new Resource() construct that occurs in a temporary right value;
  2. move assignment occurs when res = new Resource();
  3. After the move assignment, the temporary new Resource() life cycle ends, which is only the destruction of temporary objects;
  4. Resource destroy occurs at the end of main(), and res is destructed.

From this, we can compare the efficiency difference between "Copy" semantics and "Move" semantics, and understand why C++ 11 has "Move" semantics. The class we implement and STD:: unique in the standard library_ PTR is particularly similar. We will introduce it in detail in C + + memory management - Smart Pointers.

4. Call of mobile construction and mobile assignment constructor

The move construct and move assignment constructors are called only when the above function is defined and the constructed / assigned parameter is an R-value.
In addition, when the Move construct and Move assignment constructor are not explicitly defined, the default will be generated, and this set of functions with the default "Move" semantics do the same thing as the functions with the default "Copy" semantics ("Copy" and "shallow Copy"). Therefore, it is important to remember that if you want to use the Move constructor and Move assignment constructor with "Move" semantics, you must customize it.

5. Summary

This paper compares and analyzes the Copy construction, Copy assignment constructor, Move construction and Move assignment constructor. It can be seen that the "Move" semantics is superior to the "Copy" semantics in efficiency, but it should also be used reasonably according to the scene.

Welcome to criticize, comment and reprint (please indicate the source). Thank you!

Tags: C++

Posted on Wed, 01 Sep 2021 17:14:23 -0400 by zhopa