[C++11]——rvalue reference, move semantics

Table of contents

1. Basic concepts

1.1 lvalues ​​and lvalue references

1.2 Rvalues ​​and rvalue references

1.3 lvalue reference and rvalue reference

2. Practical scenarios and significance of rvalue references

2.1 Usage scenarios of lvalue references

2.2 Shortcomings of lvalue references

2.3 Rvalue references and move semantics

2.3.1 Move structure

2.3.2 Move assignment

2.3.3 Compiler optimization

2.3.4 Summary

2.4 rvalue references refer to lvalues

2.5 Other scenarios of rvalue references (insertion interface)

3. Perfect forwarding

3.1 Universal reference&&

3.2 forward perfect forwarding retains the object’s native type attributes during the parameter transfer process

3.3 Usage scenarios of perfect forwarding

1. Basic concepts

The traditional C++ syntax has reference syntax, and the new rvalue reference syntax feature is added in C++11, so from now on the references we learned before are called lvalue references. Regardless of whether it is an lvalue reference or an rvalue reference, an alias is given to the object.

1.1 lvalues ​​and lvalue references

Left value:

An lvalue is an expression that represents data (such as a variable name or a dereferenced pointer ) and has the following characteristics:

  1. We can get its address + we can assign a value to it ( may not be able to assign a value, but we can definitely get the address );
  2. An lvalue can appear on the left side of an assignment symbol, but an rvalue cannot appear on the left side of an assignment symbol;
  3. The lvalue after the const modifier when defined cannot be assigned a value, but its address can be taken .

lvalue reference:

  • An lvalue reference is a reference to an lvalue, and an alias is given to the lvalue .
int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

1.2 Rvalues ​​and rvalue references

rvalue:

An rvalue is also an expression that represents data, such as temporary variables : literal constants, expression return values, function return values ​​(this cannot be an lvalue reference return, if it is returned by value), etc., it has the following characteristics:

  1. An rvalue can appear on the right side of an assignment symbol, but cannot appear on the left side of an assignment symbol.
  2. An rvalue cannot take an address .
  3. In summary, the biggest difference between lvalues ​​and rvalues ​​is that lvalues ​​can take addresses, but rvalues ​​cannot take addresses (because rvalues ​​are temporary variables and are not actually stored ).

Replenish:

In C++, rvalues ​​are divided into two categories (pure rvalues ​​and dying values):

  1. Privalues ​​( objects of built-in types ): 10, a + b…
  2. Checkmate value ( object of custom type ):

          Passing value returns the generated copy: to_string(1234), anonymous object: string("11111"), s1 + "hello"

rvalue reference:

  • An rvalue reference is a reference to an rvalue, giving an alias to the rvalue.
int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;//字面常量
	x + y;//表达式返回值
	fmin(x, y);//函数返回值(传值返回)
 
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
 
	/*
	这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1; 
	x + y = 1; 
	fmin(x, y) = 1;
	*/
 
	/*
	这里编译会报错,右值不能取地址
	cout << &10 << endl;
	cout << &(x + y) << endl;
	cout << &fmin(x, y) << endl;
	*/
	return 0;
}
  • The address of an rvalue cannot be taken, but giving an alias to the rvalue will cause the rvalue to be stored in a specific location, and the address example: the address of the literal 10 cannot be taken, but After rr1 is referenced, the address of rr1 can be obtained, or rr1 can be modified. If you don't want rr1 to be modified, you can use const int&& rr1 to reference it. This is not the actual usage scenario of rvalue reference, and this feature is not important.
int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	rr2 = 5.5; // 报错
	return 0;
}

1.3 lvalue reference and rvalue reference

lvalue reference summary:

  1. An lvalue reference can only reference lvalues, not rvalues;
  2. But a const lvalue reference can apply to both lvalues ​​and rvalues;
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

Rvalue reference summary:

  1. Rvalue references can only reference rvalues, not lvalues;
  2. But rvalue references can refer to lvalues ​​after move;
int main()
{
	// 右值引用只能引用右值,不能引用左值。
	int&& r1 = 10;
	int a = 10;
	/*
	error C2440: “初始化”: 无法从“int”转换为“int &&”
	message : 无法将左值绑定到右值引用
	int&& r2 = a;
	*/
	// 右值引用可以引用move以后的左值
	int&& r3 = move(a);
	return 0;
}

Summarize:

  1. An lvalue reference can only reference lvalues, not rvalues;
  2. But a const lvalue reference can refer to either an lvalue or an rvalue ;
  3. Rvalue references can only reference rvalues, not lvalues;
  4. But rvalue references can refer to lvalues ​​after move .

Rvalue references greatly improve the efficiency of deep copying through move construction and move assignment. See below for details:

2. Practical scenarios and significance of rvalue references

2.1 Usage scenarios of lvalue references

Lvalue references solve the problems of excessive overhead and low efficiency caused by deep copies caused by copy construction:

  • Use lvalue references as parameters to prevent copy construction problems caused by passing parameters by value (leading to low efficiency)
  • Use an lvalue reference as the return value to prevent copy construction of the returned object (resulting in low efficiency)
void func1(cpp::string s)
{}
void func2(const cpp::string& s)
{}
int main()
{
	cpp::string s1("hello");
	func1(s1);//值传参
	func2(s1);//传引用传参
 
    // string operator+=(char ch) 传值返回存在深拷贝
    // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += 'a';//左值引用作为返回值
	return 0;
}

Summarize:

We all know that the += operator of the string class uses an lvalue reference as the return value. This avoids the copy construction caused by return by value. The reason for this is that the copy construction of the string class is a deep copy, which requires space opening, etc. The operation overhead is too high , resulting in low efficiency. Passing parameters by value also causes the problem of copy construction (deep copy). In order to avoid such a large overhead, using lvalue references can solve this problem well, because left Value reference is aliasing, which has no overhead and improves efficiency.

2.2 Shortcomings of lvalue references

 Lvalue references can avoid some unnecessary copy construction operations, but not all situations can be avoided:

  • Using lvalue references as parameters can completely avoid unnecessary copy operations when passing parameters;
  • Using an lvalue reference as a return value cannot completely avoid unnecessary copy operations when a function returns an object .

When a function returns a temporary object, you cannot use reference return, because the temporary object is destroyed when it leaves the function scope . You can only use value-passing return, and value-passing return will inevitably cause deep copy problems caused by copy construction. But there is no way to avoid it. This is the shortcoming of lvalue references . Example:

namesapce cpp
{
	cpp::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		cpp::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}

Because to_string here is returned by value, the copy constructor must be called when calling to_string, and the copy constructor implements a deep copy, which is inefficient:

int main()
{
	cpp::string ret = cpp::to_string(1234);//string(const string& s) -- 深拷贝
	return 0;
}
  • If you forcefully implement the above to_string as an lvalue reference return, then another problem will arise. My str is a temporary object. Because it is an lvalue reference return, what is returned is the alias of str. Use the alias as the return value and then copy it. The ret object is constructed, but the temporary object str goes out of scope and is destroyed by calling the destructor. Even if the value of the object can be accessed, the space no longer exists, and a memory error occurs at this time. ( You cannot return a reference to a local variable! )

To sum up, in order to solve the shortcomings of lvalue references, C++11 introduced rvalue references , but it does not simply use rvalue references as return values. String must be transformed. See below for details:

2.3 Rvalue references and move semantics

Move constructor:

The const lvalue reference constructed by string copy will receive lvalues ​​and rvalues, but the compiler follows the best match principle . If we add a separate rvalue reference version of the copy constructor so that it can only receive rvalues, according to the best match principle , when an rvalue is encountered, the copy constructor of the rvalue reference version is passed in, and when an lvalue is encountered, the copy constructor of the lvalue reference version is passed in. This solves the disadvantages caused by the lvalue reference, and the above-mentioned separate addition The function is our move constructor! ! !

Move assignment:

The operator= function uses a const lvalue reference to receive parameters, so no matter whether an lvalue or an rvalue is passed in during assignment, the original operator= function will be called. After adding move assignment, since move assignment uses an rvalue reference to receive parameters, if an rvalue is passed in during assignment, the move assignment function will be called (the best match principle). The original operator= function of string does a deep copy, while the move assignment function only needs to call the swap function to transfer resources, so the cost of calling move assignment is smaller than the cost of calling the original operator=.

2.3.1 Move structure

In order to solve the shortcomings of lvalue references, we need to add a move construct to cpp::string. The essence of the move construct is to steal the resources of the parameter rvalue (dying value). The placeholder is already there, so there is no need to do deep work. It's copied , so it's called move construction, which means stealing other people's resources to construct your own. Because the characteristic of the dying value is that it will be destroyed soon. Before you destroy it, you might as well pass your resources to others through the move structure.

  • What the move constructor has to do is to call the swap function to steal the resource passed in the rvalue. In order to better know whether the move constructor has been called, you can print a prompt statement in the function.
namespace cpp
{
	class string
	{
	public:
		//移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造,资源转移" << endl;
			swap(s);
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}
int main()
{
	cpp::string ret = cpp::to_string(1234);//转移将亡值的资源
	cpp::string s1("hello");
	cpp::string s2(s1);//深拷贝,左值拷贝时不会被资源转移
	cpp::string s3(move(s1));//转移将亡值的资源
	return 0;
}

The difference between move construction and copy construction:

  1. Before the move constructor was added, the copy constructor used const lvalue references to receive parameters, so both lvalues ​​and rvalues ​​would be passed in, which would inevitably lead to a series of shortcomings of lvalue references.
  2. After adding the move constructor, since the move constructor uses rvalue references to receive parameters, it can only receive rvalues.
  3. According to the compiler's best match principle, the lvalue is passed into the copy constructor of the lvalue reference, and the rvalue is passed into the move constructor of the rvalue reference.

2.3.2 Move assignment

Move assignment is an assignment operator overloaded function. The parameters of this function are of rvalue reference type. Move assignment also steals the resource passed in as rvalue and takes it as its own. This avoids deep copying, so it is called move assignment. , which means stealing other people’s resources to assign value to oneself.

  • Add a move assignment function to the current string class. What this function has to do is to call the swap function to steal the resources passed in the rvalue. In order to better know whether the move assignment function has been called, you can use this function in Print a prompt statement.
namespace cpp
{
	class string
	{
	public:
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}
int main()
{
	cpp::string ret;//string(string&& s) -- 移动构造,资源转移
	ret = cpp::to_string(1234);//string& operator=(string&& s) -- 移动赋值,资源转换
	return 0;
}

To distinguish between move assignment and operator=:

  1. Before move assignment was added, since the original operator= function used a const lvalue reference to receive parameters, the original operator= function would be called regardless of whether an lvalue or an rvalue was passed in during assignment.
  2. After adding move assignment, since move assignment uses an rvalue reference to receive parameters, if an rvalue is passed in during assignment, the move assignment function will be called (the best match principle).
  3. The original operator= function of string does a deep copy, while the move assignment function only needs to call the swap function to transfer resources, so the cost of calling move assignment is smaller than the cost of calling the original operator=.

Summarize:

  • After running this, we see that a move constructor and a move assignment are called. Because if an existing object is used to receive it, the compiler cannot optimize it. The cpp::to_string function will first use the str generation construct to generate a temporary object, but we can see that the compiler is smart enough to recognize str as an rvalue and call the move construct. Then assign this temporary object to ret1 as the return value of the cpp::to_string function call, and the move assignment called here.
  • Although the function is called twice here, it is only the movement of resources, and no deep copy is required, which greatly improves efficiency.

2.3.3 Compiler optimization

int main()
{
	cpp::string s = cpp::to_string(1234);
	return 0;
}

1. Let’s first look at the optimizations done by the compiler without move construction:

Not optimized:

  • If there is no move construction, then the to_string we implemented previously can only return by value. Return by value will first copy and construct a temporary object, and then use this temporary object to copy and construct the object we receive the return value. as the picture shows:

 optimization:

  • Before the C++11 standard came out, that is, in the case of C++98, there should have been two copy constructors, but the compiler optimized it, and the two consecutive copy constructors were finally optimized into one, directly taking str Copy constructors.

 2. Let’s take a look at the optimization done by the move construction compiler:

Not optimized:

  • After C++11 comes out, we assume that it is not optimized. According to previous understanding, if it is not optimized, the lvalue str will be copy-constructed to a temporary object . This temporary object is an rvalue ( dying value ), and then move construction will be performed. , that is, copy construction first and then move construction:

 optimization:

  • After the compiler optimizes here in C++11, the lvalue str will be optimized into an rvalue (turning the lvalue into an rvalue through move), and then moved and constructed to a temporary object. This temporary object is then moved and constructed to s, but The compiler will perform another optimization and directly move the lvalue str to s after identifying the rvalue. That is, only one move construction is performed :

 3. Let’s take a look at how the compiler handles move assignment:

  • When we do not use the return value of the function to construct an object, but use a previously defined object to receive the return value of the function, the test code is as follows:
int main()
{
	cpp::string ret;
	ret = cpp::to_string(1234);
	return 0;
}

At this time, the compiler will optimize the lvalue str into an rvalue (turn the lvalue into an rvalue through move), and then move the constructor to a temporary object. This temporary object will then be passed to the previously defined object through move assignment. .

The compiler does not optimize this situation here, because if an existing object is used to receive it, the compiler cannot optimize it. The cpp::to_string function will first use the str generation construct to generate a temporary object, but we can see that the compiler is smart enough to recognize str as an rvalue and call the move construct. Then assign this temporary object to ret1 as the return value of the cpp::to_string function call, and the move assignment called here.

2.3.4 Summary

  1. Deep copy of lvalue reference--copy construction/copy assignment
  2. Deep copy of rvalue reference--move construction/move assignment

After C++11, containers in STL have added move construction and move assignment.

2.4 rvalue references refer to lvalues

move function

According to the syntax, rvalue references can only reference rvalues, but must rvalue references not reference lvalues? Because: in some scenarios, you may really need to use rvalues ​​to reference lvalues ​​to implement move semantics. When you need to use an rvalue reference to refer to an lvalue, you can convert the lvalue into an rvalue through the move function. In C++11, the std::move() function is located in the <utility> header file. The name of this function is confusing. It does not move anything. Its only function is to force an lvalue into an rvalue reference. Then implement move semantics .
Definition of move function:

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
	// forward _Arg as movable
	return ((typename remove_reference<_Ty>::type&&)_Arg);
}

Notice:

  • The type of the _Arg parameter in the move function is not an rvalue reference, but a universal reference. Universal references have the same form as rvalue references, but rvalue references need to be of a certain type.
  • After an lvalue is moved, its resources may be transferred to others, so use a moved lvalue with caution.

The test is as follows:

int main()
{
	cpp::string s1("hello world");
	// 这里s1是左值,调用的是拷贝构造
	cpp::string s2(s1);//string(const string& s) -- 深拷贝
	// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
	// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
	// 资源被转移给了s3,s1被置空了。
	cpp::string s3(std::move(s1));//string(string&& s) -- 移动构造
	return 0;
}

2.5 Other scenarios of rvalue references (insertion interface)

After C++11, the insertion interface function in the STL container also adds an rvalue reference version:

 

Notice:

  • In C++98, the push_back function only has a const lvalue reference version, so this will cause both lvalues ​​and rvalues ​​to be passed into the lvalue reference version of push_back, which will inevitably cause subsequent deep copies and bring Problems such as excessive expenses.
  • After C++11 came out, the push_back function added an rvalue reference version. If an rvalue is passed into the push_back function, then when the push_back function constructs the node, this rvalue can be matched to the container's move constructor for resource allocation. Transfer, thus avoiding deep copies and improving efficiency.
int main()
{
	list<cpp::string> lt;
	cpp::string s1("1111");
	// 这里调用的是拷贝构造
	lt.push_back(s1);//string(const string& s) -- 深拷贝
	// 下面调用都是移动构造5
	lt.push_back("2222");//string(string&& s) -- 移动构造
	lt.push_back(std::move(s1));//string(string&& s) -- 移动构造
	return 0;
}

The first element s1 inserted in the above code will match the lvalue reference version of push_back. Inside the push_back function, the copy constructor of string will be called to perform a deep copy. When the two elements inserted later are passed in, rvalue, so it will match the rvalue reference version of push_back. At this time, the move constructor of string will be called inside the push_back function to transfer the resource.

3. Perfect forwarding

3.1 Universal reference&&

When && is used in a template, it does not represent an rvalue reference, but a universal reference. A universal reference can receive both lvalues ​​and rvalues.

template<typename T>
void PerfectForward(T&& t)//万能引用
{
	//……
}

The role of universal quotes:

  1. The && in the template does not represent an rvalue reference, but a universal reference , which can receive both lvalues ​​and rvalues.
  2. The template's universal reference only provides the ability to receive both lvalue references and rvalue references.
  3. However, the only function of reference types is to limit the types received, and they degenerate into lvalues ​​in subsequent uses .

Example:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);//右值
	int a;
	PerfectForward(a);//左值
	PerfectForward(std::move(a));//右值
	const int b = 8;
	PerfectForward(b);//const左值
	PerfectForward(std::move(b));//const右值
	return 0;
}

Note that I wrote four Fun functions above, namely lvalue reference, const lvalue reference, rvalue reference, and const rvalue reference. In the main function, I passed lvalue, rvalue, const lvalue, and const rvalue as parameters into the function template PerfectForward. Because its parameter type is universal reference &&, it can receive both lvalues ​​and rvalues. But the final test results were all lvalue references:

  • The lvalue and rvalue actually passed into the PerfectForward function template match the lvalue reference version of the Fun function, and the const lvalue and const rvalue passed into the PerfectForward function template both match the const lvalue reference version of the Fun function.
  • The fundamental reason for this phenomenon is that when the rvalue is referenced, it will cause the rvalue to be stored in a specific location . At this time, the rvalue can be fetched to the address and can be modified , so when the Func function is called in the PerfectForward function, t will be Recognized as an lvalue.

This means that universal references limit the types they can receive and degenerate into lvalues ​​in subsequent uses. However, if we want to be able to maintain its lvalue or rvalue attributes during the transfer process, we need to use the perfect forwarding we will learn below. .

3.2 forward perfect forwarding retains the object’s native type attributes during the parameter transfer process

If we want to retain the native type attributes of the object during the parameter passing process, we need to use the forward function:

template<typename T>
void PerfectForward(T&& t)
{
    //完美转发
	Fun(std::forward<T>(t));
    //std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
}

 After perfect forwarding, lvalues, rvalues, lvalue references, and rvalue references can be passed into the ideal function interface.

3.3 Usage scenarios of perfect forwarding

Here, drag the previously simulated list to make a test case. The previously implemented list did not write an rvalue reference version for the push_back function and insert function, so this will cause the left value to be passed regardless of whether the data is an lvalue or an rvalue. The version referenced by the value will inevitably cause a deep copy when building the node. The test code is as follows:

int main()
{
	cpp::list<cpp::string> lt;
	cpp::string s1("1111");//右值
	lt.push_back(s1);//左值
	lt.push_back("2222");//右值
	lt.push_back(std::move(s1));//右值
}

In order to avoid excessive overhead caused by deep copying, we write a separate rvalue reference version for the push_back and insert functions. We also need to write an rvalue reference version for the constructor, because creating a node requires the use of the node class constructor. function:

//节点类
template<class T>
struct list_node
{
	//……
	//右值引用节点类构造函数
	list_node(T&& val)
		:_next(nullptr)
		, _prev(nullptr)
		, _data(val)
	{}
};
template<class T>
class list
{
public:
	//……
	//右值引用版本的push_back
	void push_back(T&& xx)
	{
		insert(end(), xx);
	}
	//右值引用版本的insert
	iterator insert(iterator pos, T&& xx)
	{
		Node* newnode = new Node(xx);//创建新的结点
		Node* cur = pos._node; //迭代器pos处的结点指针
		Node* prev = cur->_prev;
		//prev newnode cur
		//链接prev和newnode
		prev->_next = newnode;
		newnode->_prev = prev;
		//链接newnode和cur
		newnode->_next = cur;
		cur->_prev = newnode;
		//返回新插入元素的迭代器位置
		return iterator(newnode);
	}
private:
	Node* _head;
}

Although the rvalue reference version is implemented here, the actual running result is still a deep copy, which is exactly the same as the running result before writing. The reasons are as follows:

  • Based on our previous understanding, we know that when && is used in a template, it does not represent an rvalue reference, but a universal reference. A universal reference can receive both lvalues ​​and rvalues. However, in subsequent use, all received types will be degenerated into lvalues. Since they are degenerated into lvalues, they will naturally enter subsequent deep copies.

This situation is a typical perfect forwarding usage scenario. The solution is as follows:

  • We need to retain the native type attributes of the object during the parameter passing process, so we need to use the forward function:
//右值引用节点类的构造函数
list_node(T&& val)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(std::forward<T>(val))//完美转发
{}
//右值引用版本的push_back
void push_back(T&& xx)
{
	//完美转发
	insert(end(), std::forward<T>(xx));
}
//右值引用版本的insert
iterator insert(iterator pos, T&& xx)
{
	//完美转发
	Node* newnode = new Node(std::forward<T>(xx));
	//……
	return iterator(newnode);
}

 

Guess you like

Origin blog.csdn.net/m0_49687898/article/details/131979303