C/C++ programming: move constructor

We know that: rvalue references are mainly used to implement move semantics and perfect forwarding . So, what move semantics? How is it achieved?

Introduce

Before C++11, if you want to initialize a new object of the same kind with other objects, you can only use the copy (copy) constructor in the class.

The implementation principle of the copy constructor is very simple, that is, to copy the same data as other objects for the new object (note that when there are pointer-type member variables in the class, the copy constructor needs to copy the pointer member in a deep copy mode. In addition, Also pay attention to handling static member variables)

for example:

#include <iostream>
using namespace std;
class demo{
    
    
public:
   demo():num(new int(0)){
    
    
      cout<<"construct!"<<endl;
   }
   //拷贝构造函数
   demo(const demo &d):num(new int(*d.num)){
    
    
      cout<<"copy construct!"<<endl;
   }
   ~demo(){
    
    
      cout<<"class destruct!"<<endl;
   }
private:
   int *num;
};
demo get_demo(){
    
    
    return demo();
}
int main(){
    
    
    demo a = get_demo();
    return 0;
}

As shown above, we have customized a copy constructor for the demo class. When this function copies the d.num pointer member, it must adopt a deep copy method, that is, while copying the pointer member itself, it also copies the memory resource pointed to by the pointer. Otherwise, once pointer members in multiple objects point to the same heap space, the space will be released multiple times when these objects are destroyed, which is not allowed.

As you can see, the program defines a get_demo() function that can return a demo object, which is used to initialize the a object in the main() main function. The entire initialization process includes the following stages:

  • Execute the demo() statement inside the get_demo() function, that is, call the default constructor of the demo class to generate an anonymous object;
  • Executing the return demo() statement will call the copy constructor to make a copy of the previously generated anonymous object and use it as the return value of the --get_demo() function (before the function body is executed, the anonymous object will be destructed and destroyed) ;
  • Execute a = get_demo() statement, call the copy constructor again, and copy the temporary object copied before to a (after this line of code is executed, the object returned by the get_demo() function will be destructed);
  • Before the program execution ends, the destructor of the demo class will be called to destroy a.

Note that most compilers currently optimize the copy operations that occur in the program, so if we run this program with VS 2017, codeblocks, and other compilers, we often see the optimized output results:

construct!
class destruct!

And the same program, if you run the g++ demo.cpp -fno-elide-constructors command on Linux (where demo.cpp is the name of the program file), you can see the complete output:

construct!                <-- 执行 demo()
copy construct!       <-- 执行 return demo()
class destruct!         <-- 销毁 demo() 产生的匿名对象
copy construct!       <-- 执行 a = get_demo()
class destruct!         <-- 销毁 get_demo() 返回的临时对象
class destruct!         <-- 销毁 a

As shown above, using the copy constructor to initialize the a object, the bottom layer actually performs 2 copies (and deep copy) operations. Of course, for temporary objects that only apply for a small amount of heap space, the execution efficiency of deep copy is still acceptable, but if the pointer member of the temporary object applies for a large amount of heap space, then two deep copy operations will inevitably affect the initialization of the a object effectiveness

In fact, this problem has persisted in C++ programs written in the C++98/03 standard. Because the generation, destruction and copy operations of temporary variables are inherently very obscure (the compiler has made special optimizations to these processes), and will not affect the correctness of the program, so it rarely enters the programmer's field of vision

So when the member functions of the class containing pointer variables use other objects to initialize similar objects, how can we avoid the efficiency problems caused by deep copy? The C++ standard introduces a solution. The standard introduces the syntax of rvalue references, which can be used to realize movement semantics.

Implementation of mobile semantics

The so-called move semantics refers to the initialization of class objects containing pointer members by moving instead of deep copying . Simply understand, move semantics is to "move for your own use" the resources owned by other objects (usually temporary objects).

Take the demo class in the previous program as an example. The members of this class contain an integer pointer member, which by default points to the heap space that holds an integer variable. When initializing a with the temporary object returned by the get_demo() function, we only need to shallow copy the num pointer of the temporary object directly to a.num, and then modify the pointer of the num pointer in the temporary object (usually it points to NULL), so This completes the initialization of a.num.

In fact, the temporary objects generated during the execution of the program are often only used to transfer data (no other use), and will be destroyed soon. Therefore, when using a temporary object to initialize a new object, we can directly move the memory resource pointed to by the pointer member it contains to the new object without having to make a new copy, which greatly improves the execution efficiency of initialization.

For example, the following program modifies the demo class:

#include <iostream>
using namespace std;
class demo{
    
    
public:
    demo():num(new int(0)){
    
    
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num)){
    
    
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num){
    
    
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
    
    
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    
    
    return demo();
}
int main(){
    
    
    demo a = get_demo();
    return 0;
}

As you can see, on the basis of the previous demo class, we manually added a constructor for it. Unlike other constructors, this constructor uses parameters in the form of rvalue references, also known as move constructors . And in this constructor, the num pointer variable adopts a shallow copy copy method, and at the same time, d.num is reset inside the function, which effectively avoids the occurrence of "the same block of space is released multiple times".

Use the g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors command to execute this program in the Linux system, and the output result is:

construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!

Through the execution results, it is not difficult to know that after adding a move constructor to the demo class, the two copy operations generated in the process of initializing the a object with a temporary object are all transferred to the move constructor to complete.

We know that non-const rvalue references can only manipulate rvalues, and temporary objects (such as function return values, lambda expressions, etc.) generated in the results of program execution have neither names nor access to their storage addresses, so they belong to rvalues. When the class contains both a copy constructor and a move constructor, if a temporary object is used to initialize the object of the current class, the compiler will call the move constructor first to complete this operation. Only when there is no suitable move constructor in the class, the compiler will go back and call the copy constructor.

In actual development, usually while customizing the move constructor in the class, an appropriate copy constructor will be customized for it, so when the user initializes the class object with rvalues, the move constructor will be called; when using When an lvalue (not an rvalue) initializes a class object, the copy constructor is called

Question: If you use lvalues ​​to initialize similar objects, but also want to call the move constructor to complete, is there a way to achieve it?

By default, lvalue initialization of similar objects can only be done through the copy constructor. If you want to call the move constructor, you must use the rvalue for initialization. In the C++11 standard, in order to meet the needs of users to use the lvalue to initialize similar objects through the move constructor, a std::move()function is introduced , which can coerce the lvalue to the corresponding rvalue, so that the move constructor can be used

Guess you like

Origin blog.csdn.net/zhizhengguan/article/details/115014024