C + + -- right value reference

1, Basic knowledge

1. Function

Function: R-value reference and mobile semantics are referenced in C++11, which can avoid unnecessary replication and improve program performance.

2. Basic concept of right value and discrimination from left value

① All values in C++11 must belong to one of the left value, the dead value and the pure right value, and both the dead value and the pure right value belong to the right value.
② The left value refers to the persistent object that still exists after the end of the expression, and the right value refers to the temporary object that does not exist at the end of the expression.
③ A convenient way to distinguish between left and right values is to see whether the expression can be addressed. If it can, it is a left value, otherwise it is a right value.
④ The dead value is a new expression added in C++11 and related to the right value reference, such as the object to be moved, the value returned by the T & & function, the return value of std::move and the return value of the conversion function converted to the type of T &.
⑤ Lvalues are expressions that have identifiers and can take addresses. The most common cases are: the name of a variable, function or data member returns the expression referenced by lvalues, such as + + X, x = 1, cout < < ', and string literals such as "hello world".
Pure right value is an expression that has no identifier and cannot take an address. It is generally called "temporary object". The most common cases are: expressions that return non reference types, such as x + +, x + 1, and make_shared(42). Literals other than string literals, such as 42 and true.

3. Characteristics of right value reference

① An R-value reference is a type that references an R-value. Because the right value has no name, we can only find it by reference.
② Whether an lvalue reference or an lvalue reference is declared, it must be initialized immediately, because the reference type itself does not have the memory of the bound object, but only an alias of the object.
③ Through the declaration of the right value reference, the right value is "reborn", and its life cycle is the same as that of the right value reference type variable. As long as the variable is still alive, the right value temporary quantity will always survive.
&&The results are summarized as follows:
(1) Left and right values are independent of their types, and the right value reference type may be left or right.
(2) Auto & & or T & & automatically derived from function parameter type is an undetermined reference type, called universal references. It may be an lvalue reference or an lvalue reference type, depending on the initialized value type.
(3) All right value references superimposed on the right value reference are still a right value reference, and other reference folds are left value references. When T & & is a template parameter, enter a left value, it will become a left value reference, and when you enter a right value, it will become a named right value reference.
(4) The compiler treats named R-value references as lvalues and unnamed R-value references as lvalues.

2, R-value reference optimizes performance

For classes with heap memory, we need to provide a deep copy copy constructor. If the default constructor is used, the heap memory will be deleted repeatedly, such as the following code:

#include <iostream>
using namespace std;
class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	~A() {
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int* m_ptr;
};

// To avoid return value optimization, this function is written intentionally
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}

int main()
{
	{
		A a = Get(false); // Operation error
	}
	cout << "main finish" << endl;
	return 0;
}

In the above code, the default constructor is a shallow copy, and a of the main function and b of the Get function point to the same pointer m_ptr, which will cause the pointer to be deleted repeatedly during destruct. The solution is to use a deep copy. The following deep copy code:

//2-1-memory2
#include <iostream>
using namespace std;
class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	A(const A& a) :m_ptr(new int(*a.m_ptr)) {
		cout << "copy constructor A" << endl;
	}
	~A() {
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int* m_ptr;
};

// To avoid return value optimization, this function is written intentionally
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}

int main()
{
	{
		A a = Get(false); // Correct operation
	}
	cout << "main finish" << endl;
	return 0;
}

Operation results:

This can ensure the security of the copy structure, but sometimes this copy structure is unnecessary. For example, the copy structure in the above code is unnecessary. The Get function in the above code will return a temporary variable, and then copy and construct a new object b through this temporary variable. The temporary variable will be destroyed after the copy and construction is completed. If the heap memory is large, the cost of this copy and construction will be very high,
It brings additional performance loss. You can optimize with an R-value reference:

#include <iostream>
using namespace std;
class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	A(const A& a) :m_ptr(new int(*a.m_ptr)) {
		cout << "copy constructor A" << endl;
	}
	A(A&& a) :m_ptr(a.m_ptr) {
		a.m_ptr = nullptr;
		cout << "move constructor A" << endl;
	}
	~A() {
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		if (m_ptr)
			delete m_ptr;
	}
private:
	int* m_ptr;
};
// To avoid return value optimization, this function is written intentionally
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}
int main()
{
	{
		A a = Get(false); // Correct operation
	}
	cout << "main finish" << endl;
	return 0;
}

Operation results:

3, Mobile semantics (move) and perfect forwarding (forward)

1. Mobile semantic move

Mobile semantics matches temporary values through right value references. Can ordinary lvalues also use group mobile semantics to optimize performance? In order to solve this problem, C++11 provides the std::move() method to convert the left value to the right value, so as to facilitate the application of mobile semantics. Move is to transfer the state or ownership of an object from one object to another. It is just an escape without memory copy.

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string>
using namespace std;
class MyString {
private:
	char* m_data;
	size_t m_len;
	void copy_data(const char* s) {
		m_data = new char[m_len + 1];
		memcpy(m_data, s, m_len);
		m_data[m_len] = '\0';
	}
public:
	MyString() {
		m_data = NULL;
		m_len = 0;
	}
	MyString(const char* p) {
		m_len = strlen(p);
		copy_data(p);
	}
	MyString(const MyString& str) {
		m_len = str.m_len;
		copy_data(str.m_data);
		cout << "Copy Constructor is called! source: " << str.m_data << endl;
	}
	MyString& operator=(const MyString& str) {
		if (this != &str) {
			m_len = str.m_len;
			copy_data(str.m_data);
		}
		cout << "Copy Assignment is called! source: " << str.m_data << endl;
		return *this;
	}
	// The two functions are defined by the right value reference of c++11
	MyString(MyString&& str) {
		cout << "Move Constructor is called! source: " << str.m_data << endl;
		m_len = str.m_len;
		m_data = str.m_data; //Avoid unnecessary copies
		str.m_len = 0;
		str.m_data = NULL;
	}
	MyString& operator=(MyString&& str) {
		cout << "Move Assignment is called! source: " << str.m_data << endl;
		if (this != &str) {
			m_len = str.m_len;
			m_data = str.m_data; //Avoid unnecessary copies
			str.m_len = 0;
			str.m_data = NULL;
		}
		return *this;
	}
	virtual ~MyString() {
		if (m_data)
			free(m_data);
	}
};

int main()
{
	MyString a;
	a = MyString("Hello"); //Nameless object, right value, Move Assignment
	MyString b = a; // Copy Constructor
	MyString c = std::move(a); // Move Constructor to change the left value to the right value

	return 0;
}

Operation results:

2. Perfect forward

forward perfect forwarding realizes the function of maintaining the value attribute of parameters during transmission, that is, if it is an lvalue, it will still be an lvalue after transmission, and if it is an lvalue, it will still be an lvalue after transmission. The so-called perfect forwarding means that in the function template, the left or right value characteristics of the parameters are maintained exactly according to the type of the template parameter, and the parameters are passed to another function called in the function template, regardless of whether the parameter is T&&'s undetermined reference or explicit left value reference or right value reference, it will be forwarded according to the original type of the parameter.

(1) According to the above description, the following reference types can refer to both lvalues and rvalues. However, it should be noted that after reference, the val value is essentially an lvalue!

Template<class T>
void func(T &&val);

(2) Note that in the following statement, a is an R-value reference, but a itself also has a memory name, so a itself is an l-value, and it is wrong to refer to a with an R-value reference.

int &&a = 10;
int &&b = a; //error

(3) Therefore, we have std::forward() perfect forwarding. val in T & & val is an lvalue, but if we use std::forward (val), it will be forwarded according to the original type of the parameter.

int &&a = 10;
int &&b = std::forward<int>(a);

(4) Perfect forwarding is further illustrated by an example

#include <iostream>
using namespace std;

template <class T>
void Print(T& t)
{
	cout << "L" << t << endl;
}

template <class T>
void Print(T&& t)
{
	cout << "R" << t << endl;
}


//You can reference either an lvalue or an lvalue. But note that after reference, the t value is essentially an lvalue
template <class T>
void func(T&& t)
{
	Print(t);//It must be an lvalue, because t is already a named variable
	Print(std::move(t));//move(t) is the right value
	Print(std::forward<T>(t));//forward(t) forwards according to the original type of the parameter
}

int main()
{
	cout << "-- func(1)" << endl;
	func(1);//Right value
	int x = 10;
	int y = 20;
	cout << "-- func(x)" << endl;
	func(x); // x itself is an lvalue
	cout << "-- func(std::forward<int>(y))" << endl;
	func(std::forward<int>(y)); //
	return 0;
}

Operation results:

func(1): since 1 is an R-value, the undetermined reference type T & & T becomes an R-value reference after being initialized by an R-value, but t is already a named l-value at this time. So Print(1) is L1; Print(move(t)). At this time, t is an lvalue, but move(t) will return an lvalue attribute, so Print(move(t)) prints R1; Print(forward(t)), forward will be forwarded according to the original type of the parameter. Type derivation has occurred here. Therefore, it is still an R-value, so t & & here is not an undetermined reference type. void PrintT (T & & T) function will be called to print "R1".

ditto:
The undefined reference type T & & T of func(x) becomes an lvalue reference after being initialized by an lvalue. Therefore, Print(t) is L10; Print(move(t)) is R10; Print(forward(x)) is L10, because forward(t) will automatically push the type to the original attribute of the parameter, and X is an lvalue.

The undefined reference type T & & T of func(forward(y)) is initialized by an lvalue and becomes an lvalue reference. Therefore, Print(t) is L20; Print(move(t)) is R20; Print(forward(x)) is R20, because forward < int > (T) in func() function will automatically push the type to the original attribute of the parameter, and forward < int > y is the right value when passing the parameter, so R20 is printed.

Keywords: C++

Added by gortron on Fri, 24 Dec 2021 14:21:45 +0200