Rvalue references, move constructors, and move

Left value and right value

Left value and right value judgment:
1) The expression that can be located on the left side of the assignment number (=) is the left value; otherwise, the expression that can only be located on the right side of the assignment number is the right value.
2) The expression that has a name and the storage address can be obtained is an lvalue; otherwise, it is an rvalue.
E.g:

int i = 10;
10 = i;
错误,10为右值,不能当左值用
int j = 20;
j = i;
i和j都是左值,但是i可以当右值用

  Take the variables i and j defined above as an example, i and j are variable names, and their storage addresses can be obtained through &a and &b, so a and b are both lvalues; on the contrary, the literals 10 and 20 have neither The name, the storage address cannot be obtained (the literal quantity is usually stored in a register or stored together with the code), so 10 and 20 are both rvalues.
  Note that the above two judgment methods are only applicable to most scenarios.

Rvalue reference

int i = 10;
int &a = i;
int &b = 10;//错误
const int &c = 10;//正确

The regular & we use represents lvalue references, ordinary lvalue references can only receive lvalues, not rvalues, but constant lvalue references can receive lvalues ​​and rvalues.

int j = 10;
int &&d = 10;
int &&e = j;//错误

The two & represent rvalue references. Regular rvalue references can be used to receive rvalues, but cannot be used to receive lvalues
Insert picture description here
. The figure above describes the data types that lvalue references and rvalue references can receive, including non-constant rvalue references. Commonly used for mobile constructor and perfect forwarding.

Move constructor

Look at the following scene:

#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()
	{
    
    
		if(num != nullptr)
		{
    
    
			delete num;
			num = nullptr;
		}
		cout << "class destruct!" << endl;
	}
private:
	int *num;
};
demo get_demo()
{
    
    
	return demo();
}
int main()
{
    
    
	demo a = get_demo();
	return 0;
}

It can be seen that the program runs twice to call the copy constructor, the first time is in the get_demo function, the second time is when the a object is initialized, and these two copies are deep copies, you can see in Linux Run result:
Use this command to compile the file, otherwise you will not see the complete output result

g++ main.cpp -fno-elide-constructors

Insert picture description here
Imagine that if a lot of memory space is requested on the heap during the two deep copy processes, a lot of time will be wasted and the program efficiency is low. Then how to optimize this problem. In fact, think about it carefully. The two deep copy processes generate all An anonymous object cannot get the address through &, so it is an rvalue, and the program can be optimized by introducing a move constructor.

#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 = nullptr;
		cout << "move construct!" << endl;
	}
	~demo()
	{
    
    
		if(num != nullptr)
		{
    
    
			delete num;
			num = nullptr;
		}
		cout << "class destruct!" << endl;
	}
private:
	int *num;
};
demo get_demo()
{
    
    
	return demo();
}
int main()
{
    
    
	demo a = get_demo();
	return 0;
}

Check the execution results of the above program:
Insert picture description here
  you can see that the two deep copy processes are completed by the move constructor. In the move constructor, the parameter is a demo rvalue reference, and the pointer of the newly generated object is directly The member points to the heap space requested by the anonymous object, and the member pointer of the anonymous object is blanked. This prevents each call to the copy constructor from applying for new memory space on the heap, which greatly improves efficiency.
  The so-called move semantics refers to the initialization of class objects containing pointer members by moving instead of deep copying . In simple understanding, move semantics refers to "moving to used" memory resources owned by other objects (usually temporary objects).

move

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 using the move constructor to initialize similar objects with lvalues, the std::move() function is newly introduced, which can coerce the lvalue into the corresponding rvalue . Then you can use the move constructor.

#include <iostream>
using namespace std;
class MoveDemo
{
    
    
public:
	MoveDemo() :num(new int(0))
	{
    
    
		cout << "construct!" << endl;
	}
	MoveDemo(const MoveDemo &d) :num(new int(*d.num))
	{
    
    
		cout << "copy construct!" << endl;
	}
	//添加移动构造函数
	MoveDemo(MoveDemo &&d) :num(d.num)
	{
    
    
		d.num = nullptr;
		cout << "move construct!" << endl;
	}
	~MoveDemo()
	{
    
    
		if (num != nullptr)
		{
    
    
			delete num;
			num = nullptr;
		}
		cout << "class destruct!" << endl;
	}
public:
	int *num;
};
MoveDemo get_demo()
{
    
    
	return MoveDemo();
}
int main()
{
    
    
	MoveDemo demo;
	cout << "demo2:\n";
	MoveDemo demo2 = demo;
	//cout << *(demo2.num) << endl;   //可以执行
	cout << "demo3:\n";
	MoveDemo demo3 = std::move(demo);
	//此时 demo.num = NULL,因此下面代码会报运行时错误
	//cout << *(demo.num) << endl;
	return 0;
}

Insert picture description here
It can be seen that the move constructor is called in the process of initializing demo3. The reason is to use move to force the demo lvalue into an rvalue.

Guess you like

Origin blog.csdn.net/bureau123/article/details/112696446