[C++]——C++11 new features "rvalue reference and move semantics"

Foreword:

  • In this issue, we will introduce relevant knowledge about C++ rvalue references. For the knowledge content of this issue, everyone must be able to master it, and it is a key object of investigation in the interview.

Table of contents

(1) lvalue reference and rvalue reference

1. What is an lvalue? What is an lvalue reference?

2. What is an rvalue? What is an rvalue reference?

(2) Comparison between lvalue reference and rvalue reference

(3) Usage scenarios and significance of rvalue references

(4) Perfect forwarding 

1. Concept

2. The && universal reference in the template

3、std::forward

Summarize


(1) lvalue reference and rvalue reference

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. What is an lvalue? What is an lvalue reference?

There are references in the C++98/03 standard, which are represented by "&" . However, this reference method has a flaw, that is, under normal circumstances, only lvalues ​​in C++ can be operated, and references cannot be added to rvalues . for example:
 

int main()
{
	int num = 10;
	int& b = num; //正确
	int& c = 10; //错误

	return 0;
}

Output display:

 【explain】

  • As shown above, the compiler allows us to create a reference to the num lvalue, but not to the 10 rvalue. Therefore, references in the C++98/03 standard are also called lvalue references.
     

So what exactly is an lvalue? What is an lvalue reference?

  1. An lvalue is an expression that represents data (such as a variable name or a dereferenced pointer). We can get its address + we can assign a value . An lvalue can appear on the left side of the assignment symbol, and an rvalue cannot appear on the left side of the assignment symbol. ;
  2. The lvalue after the const modifier when defined cannot be assigned a value, but its address can be taken. An lvalue reference is a reference to an lvalue, and an alias is given to the lvalue.
     

Note : Although the C++98/03 standard does not support the establishment of non-const lvalue references to rvalues, it does allow the use of const lvalue references to operate on rvalues. That is to say, a constant lvalue reference can operate on both lvalues ​​and rvalues, for example:
 

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;
}

2. What is an rvalue? What is an rvalue reference?
 

We know that rvalues ​​often have no names , so they can only be used by reference. This creates a problem. In actual development, we may need to modify the rvalue (required when implementing move semantics). Obviously, the lvalue reference method does not work.


To this end, the C++11 standard introduces another reference method, called an rvalue reference, represented by "&&":

  • An rvalue is also an expression that represents data , such as: literal constant, expression return value, function return value (this cannot be an lvalue reference return), etc.;
  • An rvalue can appear on the right side of an assignment symbol, but cannot appear on the left side of an assignment symbol. An rvalue cannot take an address ;
  • 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);

	return 0;
}

Output display:

 But if it is the following expressions, an error will occur:

10 = 1;
x + y = 1;
fmin(x, y) = 1;

The output shows:

It should be noted that the address of an rvalue cannot be obtained , but giving an alias to the rvalue will cause the rvalue to be stored in a specific location, and the
address of that location can be obtained. For example, the following code shows:

int main()
{
	double x = 1.1, y = 2.2;

	int&& rr1 = 10;
	double&& rr2 = x + y;
	cout << rr1 << " " << rr2 << " " << endl;

	rr1 = 20;
	rr2 = 5.5;
	cout << rr1 << " " << rr2 << " " << endl;

	return 0;
}

Output display:

 When we don’t want to be modified, we can add the [const] keyword:

【explain】

  1. The address of literal 10 cannot be taken, but after rr1 is referenced, the address of rr1 can be taken, or rr1 can be modified;
  2. If you don't want rr1 to be modified, you can use const int&& rr1 to reference;
  3. Does it feel amazing? This is not the case for understanding the actual use of rvalue references, and this feature is not important.
     

(2) Comparison between lvalue reference and rvalue reference

lvalue reference summary:

  • 1. Lvalue references can only refer to lvalues, not rvalues.
  • 2. But const lvalue references can refer to both lvalues ​​and rvalues

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
    
    return 0;
}

Output display:

 Another example is the following:

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra2 = 10; // 编译失败,因为10是右值

    return 0;
}

Output display:

 Lvalue references can only refer to lvalues, not rvalues. But when we add const, then the lvalue reference can alias the rvalue:

int main()
{
    int a = 10;
    // const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
    
    return 0;
}

Output display:

【explain】

It is worth mentioning that although C++ syntax supports the definition of constant rvalue references, such defined rvalue references have no practical use:

const int& ra3 = 10;
  1. On the one hand, rvalue references are mainly used for move semantics and perfect forwarding , where the former requires permission to modify rvalues;
  2. Secondly, the function of a constant rvalue reference is to refer to an unmodifiable rvalue. This work can be completed by a constant lvalue reference.
     


Rvalue reference summary:

  • 1. Rvalue references can only refer to rvalues, not lvalues.
  • 2. But rvalue references can move subsequent lvalues.
     

Code display:

 An error will occur when referencing an lvalue:


Converting an lvalue to an rvalue reference is supported by move 

 In C++, movea function template that converts a given object into the corresponding rvalue reference. It does not perform the actual memory move operation, but marks the object as an rvalue that can be moved. In this way, users can leverage this markup to achieve more efficient move semantics.


(3) Usage scenarios and significance of rvalue references

We can see earlier that lvalue references can reference both lvalues ​​and rvalues, so why does C++11 also propose rvalue references? Is it superfluous? Let's take a look at the shortcomings of lvalue references and how rvalue references can make up for these shortcomings!

The following code exists: 

 【explain】

First, for res1 and res2 in the above code, they are lvalues ​​and rvalues ​​respectively;

Next, let's think about it, is there any difference between copying lvalues ​​and rvalues?

  • If it is a built-in type, the difference between them is not very big, but for a custom type, the difference is very big.
  • Because of the rvalue of the custom type, it is generally called a dying value in many places. Usually it is the return value of some expressions, a function call, etc.;
  • As for rvalues, they are divided into pure rvalues ​​(generally speaking, built-in types) and dying values ​​(generally speaking, custom types).

For the above res1, as an lvalue, we cannot operate on it and can only do deep copy. Because although it seems like an assignment here, it should actually be a copy construction;

As for res2, it itself is an rvalue. If it is a custom type as a dying value, we do not need to copy it. At this point, the concept of rvalue reference implementation triggering construction is introduced.


For example, there is now a string that we simulate by handwriting:

namespace zp
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}


		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

When we observe the above res1 and res2 in this scenario:

 We can find that the rvalue here is a corresponding deep copy, which will obviously cause unnecessary waste. In order to solve the above problems, we can introduce the concept of "move construction":

// 移动构造
string(string&& s)
	:_str(nullptr)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;
	swap(s);
}

Immediately afterwards, running the above code again, we can find that the compiler will automatically recognize:

At this point, what can we do when we just want to convert s1 into an rvalue? It's actually very simple ( move is reflected here ):

 Output display:

We can also find through debugging that the expected effect is indeed achieved at this time:

 【summary】

From the above we can find that the benefit of lvalue references is to directly reduce copying

The usage scenarios of lvalue references can be divided into the following two parts:

  • Both parameters and return values ​​can improve efficiency

Shortcomings of lvalue references:

  • But when the function return object is a local variable, it does not exist outside the function scope, so you cannot use lvalue reference to return, and can only return by value.

For example, we now have the following code: 

    zp::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		zp::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;
	}

【illustrate】

  • As can be seen in the zp::string to_string(int value) function, only return by value can be used here. Return by value will result in at least one copy construction (maybe two copy constructions if it is some older compilers ) .

 Next, let's print to see what the result is:

 At this time, the cost of return by value has been greatly solved:

 Rvalue references and move semantics solve the above problems:

  • Add a move structure to zp::string. The essence of the move structure is to steal the resource of the rvalue of the parameter. If the placeholder already exists, then there is no need to do a deep copy, so it is called a move structure, which is to steal other people's resources to construct itself. ;

Then run the two calls of zp::to_string above, and we will find that the copy construction of deep copy is not called here, but the move construction is called. There is no new space in the move construction to copy data, so the efficiency is improved.


C++11 not only has move construction, but also move assignment:

Add a move assignment function to the zp::string class, and then call zp::to_string(1234), but this time the rvalue object returned by zp::to_string(1234) is assigned to the ret1 object, and the move is called at this time structure.

Output display:

 【explain】

  • 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 zp::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 as the return value of the zp::to_string function call to ret1, and the move assignment called here.

(4) Perfect forwarding 

1. Concept

  • Perfect forwarding is a feature introduced in C++11, aiming to achieve the ability to accurately pass parameter types in function templates;
  • It is mainly used to preserve the value category of the actual parameters passed to the function template and forward it to the internally called function, thereby achieving complete preservation of type and value category;

for example:

template<typename T>
void PerfectForward(T t)
{
    Fun(t);
}

【explain】

  1. As shown above, the Func() function is called in the PerfectForward() function template;
  2. On this basis, perfect forwarding refers to: if the parameter t received by the PerfectForward() function is an lvalue, then the parameter t passed by the function to Func() is also an lvalue;
  3. On the other hand, if the parameter t received by the function() function is an rvalue, then the parameter t passed to the Func() function must also be an rvalue.

Using either form of citation, forwarding can be achieved, but perfection is not guaranteed. Therefore, if we use the C++ language under the C++98/03 standard, we can use function template overloading to achieve perfect forwarding, for example:

template<typename T>
void Func(T& arg)
{
    cout << "左值引用:" << arg << endl;
}

template<typename T>
void Func(T&& arg)
{
    cout << "右值引用:" << arg << endl;
}

template<typename T>
void PerfectForward(T&& arg)
{
    Func(arg);  // 利用重载的process函数进行处理
}

int main()
{
    int value = 42;
    PerfectForward(value);       // 传递左值
    PerfectForward(123);         // 传递右值

    return 0;
}

Output display:

 【explain】

  1. In the above example, we defined two overloaded function templates  Func, one receiving lvalue reference parameters T& argand the other receiving forward reference parametersT&& arg;
  2. Then, we define a template function PerfectForwardwhose parameters are also forwarded references T&& arg. Inside PerfectForwardthe function, we Funcprocess the passed parameters by calling the function;
  3. Through the function overloading mechanism, the passed lvalue parameters will match the function that receives the lvalue reference Func, and the passed rvalue parameter will match Functhe function that receives the forwarded reference, so that they can be distinguished and processed correctly;
  4. Through the overloading of function templates, we can distinguish lvalues ​​and rvalues ​​according to parameter types and process them separately, achieving precise matching and operations for different value categories.

2. The && universal reference in the template

Obviously, the above-mentioned use of overloaded template functions to achieve perfect forwarding also has disadvantages. This implementation method is only suitable for situations where the template function has only a few parameters. Otherwise, a large number of overloaded function templates will need to be written, causing code redundancy. In order to facilitate users to achieve perfect forwarding more quickly, the C++11 standard allows the use of rvalue references in function templates to achieve perfect forwarding.

Let’s take the PerfectForward() function as an example. To achieve perfect forwarding in the C++11 standard, you only need to write the following template function:

//模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}

Take the following code as an 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;
}

Output display:

 【explain】

  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.
  4. 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、std::forward

The developers of the C++11 standard have already thought of a solution for us. The new standard also introduces a template function forword<T>(). We only need to call this function to easily solve this problem.

  1. Perfect forwarding is usually used with forwarding reference and std::forward function;
  2. A forwarding reference is a special type of reference that is &&declared using a syntax used to capture passed arguments in function templates;
  3. std::forward is a template function used to forward a forward reference as an rvalue or lvalue reference inside a function template.

The following demonstrates the usage of this function template:

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)
{
	// forward<T>(t)在传参的过程中保持了t的原生类型属性。
	Fun(std::forward<T>(t));
}
int main()
{
	PerfectForward(10); // 右值

	int a;
	PerfectForward(a); // 左值
	PerfectForward(move(a)); // 右值

	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(move(b)); // const 右值
	return 0;
}

The program execution result is:

Through perfect forwarding, we can correctly handle the value category of the passed actual parameter in the function template and forward it to the internal function to achieve complete preservation of the type and value category, improving the flexibility and efficiency of the code.


Summarize

After learning this, some readers may not be able to remember clearly whether lvalue references and rvalue references can refer to lvalues ​​or rvalues. Here is a table for everyone to facilitate their memory:
 

  •  In the table, Y means supported and N means not supported.

The above is all the knowledge about lvalue references and rvalue references! Thank you for watching and supporting! ! !

Guess you like

Origin blog.csdn.net/m0_56069910/article/details/132475156