C++ grammar - rvalue reference, move construction and assignment, universal reference and forwarding, underlying implementation of move and forward

Table of contents

1. Rvalue references

(1). What is an rvalue

(2). Rvalue references

(3). Mutual transmission of rvalues ​​and lvalues

① Lvalue -> rvalue reference

②Rvalue -> lvalue reference

(4). Own properties of rvalue references

2. Move construction and move assignment

 (1). Mobile structure

 (2). Mobile assignment

3. Forward

(1). Universal reference

(2). Perfect forwarding

4. The underlying implementation of move and forward

(1) The underlying implementation of .move

(2) The underlying implementation of .forward


1. Rvalue references

(1). What is an rvalue

What cannot be addressed is an rvalue. For example: literal constants, temporary variables.

//1就是右值
int i = 1;
//max返回值是临时变量,也就是右值
int n = max(1, 2);

(2). Rvalue references

An lvalue reference is a reference to an lvalue, and as the name suggests, an rvalue reference is a reference to an rvalue.

The rvalue reference symbol is && , which is used in the same way as an lvalue reference, but the symbol and reference object properties are different.

lvalue reference rvalue reference
symbol & &&
reference object lvalue (variable whose address can be taken) rvalue (not addressable)
How to use

int j = 1;

int& a = j;

int&& a = 1;

(3). Mutual transmission of rvalues ​​and lvalues

Lvalues ​​and rvalues ​​cannot be passed directly.

int main() 
{
	int a = 1;
	int& b = a;//正确,左值引用

	int&& c = b;//错误。右值引用不能传左值

	int& d = 1;//错误。左值引用不能传右值
	return 0;
}

① Lvalue -> rvalue reference

The function parameter is an lvalue and the return value is an rvalue.

The function is to force the lvalue parameter into an rvalue reference

int a = 1;
int&& c = move(a);//将a强转成右值

②Rvalue -> lvalue reference

Lvalue references can be added with the const modifier.

const int& d = 1;

(4). Own properties of rvalue references

An rvalue reference is itself an lvalue property.

Therefore, it is okay to pass an rvalue reference to an lvalue reference.

int&& a = 1;//a为右值引用,但自身属性是左值
int& b = a;//正确,给左值引用传递左值

How should this be understood?

"C++ Primer" gives a relevant explanation for this, the general idea is as follows:

Lvalues ​​are "persistent", rvalues ​​are "short-lived". That is, lvalues ​​can always exist as long as they are out of scope, but rvalues ​​can only "survive" at the moment of use (refer to function return value).

Therefore, when an rvalue reference is made, the reference itself can always exist in the scope, then it is an lvalue.

Of course, another way can be used to prove: take the address.

An lvalue can take an address, but an rvalue cannot take an address.

int main() 
{
	int a = 1;//a可以取地址,是左值
	int&& b = 3;
	cout << "a地址: " << &a << endl;
	cout << "b地址: " << &b << endl;
	return 0;
}

 May wish to summarize:

lvalue rvalue lvalue reference rvalue reference
example int a = 1; string str = "abc"; int& b = a; int&& c = 1;
Attributes lvalue rvalue lvalue lvalue
take address able cannot able able
convert

Receive an rvalue:

direct pass

Receive an lvalue:

none

Receive an rvalue:

+const

Receive an lvalue:

move function 

2. Move construction and move assignment

After the introduction of rvalue references in C++11, the most important role is the move construction and assignment.

For example, related functions are provided in the official STL library:

 (1). Mobile structure

The purpose of the move construction is to reduce the problem of repeated copying caused when the parameter is an lvalue. 

Take string as an example to illustrate:

Intercept the following code:

class String {

	...

	explicit String(const char* a = "")//默认构造
	{
		_size = strlen(a);
		_capacity = _size;
		_a = new char[_capacity + 1];
		strcpy(_a, a);
		cout << "构造函数\n";
	}

	String(const String& st)//拷贝构造
		:_a(nullptr)
	{
		String tmp(st.c_str());//调用构造函数
		swap(tmp);
		cout << "拷贝构造\n";
	}

    ...

	
};
String To_string(int value)//将int转为string
{
    ...
    String str;
    ...
    return str;
}
int main()
{
	String str = To_string(20);
	return 0;
}

When we execute this program, 2 default constructors and 1 copy constructor are called:

The default construction is called when to_string internally generates str, and the string copy construction is called when the temporary variable is returned, but the default construction will be called first inside the string copy construction.

In fact, this is still after optimization. If there is no compiler optimization, str in the main function will also call the string copy construction again.

 And what is the "culprit" of all this? - the return value of to_string.

Yes, because to_string will generate a string object inside, and this object is a local variable, which will be destroyed when it goes out of the scope of the function, so it can only call to copy and construct the object inside to_string.

This is only the copy construction of the string type. If it is a more complex type, the copy construction will often cause more resource occupation.

That's where move construction comes in handy:

String(String&& st)//移动构造函数,但是参数为右值
	:_a(nullptr)
{
	swap(st);
	cout << "string移动构造\n";
}

The parameters of the move construction are rvalues, so when to_string returns str, it will be received by the move construction.

Although str itself is an lvalue attribute, because str is a "dying value" at this time, that is, it will be destroyed when it goes out of the scope of the function, and the compiler will recognize this "dying" value as an rvalue.

Inside the move construction, the data of the rvalue is exchanged with its own data . Because the rvalue is used as "temporarily existing data", the data is given to the target object, and the target object gives the "abandoned" data to the rvalue, which can just "continue" the target data and eliminate the original data.

At this time, when receiving the return value of to_string, you only need to move the structure one by one: 

 (2). Mobile assignment

The purpose of move assignment is similar to move construction, which is to reduce the problem of repeated copying caused by assignment .

Take string as an example, where the assignment overload is implemented by calling the copy constructor.

class String {

	...

	String& operator=(const String& st)//赋值重载1
	{
		String tmp(st);//调用拷贝构造
		swap(tmp);
		cout << "string赋值\n";
		return *this;
	}
	String& operator=(const char* str)//赋值重载2
	{
		String tmp(str);
		swap(tmp);
		cout << "char*赋值\n";
		return *this;
	}

    ...

	
};
int main()
{
	String str;
	cout << "--------------------------------\n";
	str = To_string(1);
	return 0;
}

 When executing this program, multiple constructors, copy constructors, are called:

 Among them, there are three that are called because of assignment overloading.

 Because the parameter of the assignment overload is an lvalue reference, data cannot be exchanged like an rvalue reference, and the data can only be obtained by calling the copy construction.

As a result, mobile assignment came into being:

Like move construction, move assignment also directly exchanges data with rvalues. 

String& operator=(String&& st)
{
	swap(st);
	cout << "string移动赋值\n";
	return *this;
}

 At this point, you only need to pass the return value of to_string as an rvalue to the move assignment.

3. Forward

(1). Universal reference

First, universal references only exist with template programming .

A universal reference is a reference parameter that can receive both an lvalue and an rvalue. Its symbol is the same as an rvalue reference, but it must be a template.

That is, when the parameter of the template is in the form of an rvalue reference, if the actual parameter is an lvalue, it is an lvalue reference, and an rvalue is an rvalue reference.

 For example the following code:

void Print(int& a)
{
	cout << "左值" << endl;
}

void Print(int&& a)
{
	cout << "右值" << endl;
}

template<class T>
void func(T&& t)//万能引用
{
	Print(t);
}

int main()
{
	int a = 0;
	func(a);//传左值
	func(1);//传右值
	return 0;
}

(2). Perfect forwarding

There is a problem with the above code. Although func(1) passes in an rvalue, because the rvalue reference itself is an lvalue, when the Print function is called, the lvalue version will be called, which does not meet our expectations, because it is clearly passed The input is an rvalue:

 At this time, you need to use perfect forwarding forward, which will keep the attributes of the incoming actual parameters unchanged:

void func(T&& t)
{
	Print(std::forward<T>(t));
}

4. The underlying implementation of move and forward

(1) The underlying implementation of .move

First look at the underlying code of the move function:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_case<typename remove_reference<T>::type&&>(t);
}

Among them, the parameter T&& is a universal reference, which can receive lvalue or rvalue.

The return value is very special. The meaning of typename remove_reference <T>::type is to remove the reference type of T.

remove_reference itself is a template class, and its function is to return a type, so there are only member types in this class .

From the source code of remove_reference, we can see that no matter whether an lvalue reference or an rvalue reference is passed in, it will only return the type of this value after removing the reference.

Let's take int as an example. Regardless of whether int& or int&& is passed in, after remove_reference<T>, all ints are returned.

template <typename T>
struct remove_reference{
    typedef T type;  //成员类型
};

template <typename T>
struct remove_reference<T&> //左值引用
{
    typedef T type;//返回T本身的类型
}

template <typename T>
struct remove_reference<T&&> //右值引用
{
   typedef T type;//返回T本身的类型
}

The role of static_case is to force type conversion , which can force an lvalue to an rvalue , and move to an rvalue reference.

Therefore, the underlying code of move can be translated into the following form:

template <typename T>
int&& move(T&& t)
{
    return (int&&)(t);
}

Therefore, we clearly found that the move function is implemented by obtaining the type of the reference object itself through remove_reference and forcing it into an rvalue reference .

(2) The underlying implementation of .forward

This is the underlying code of forward:

template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)//左值引用
{
    return static_cast<T&&>(param);//万能引用
}

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)//右值引用
{
    return static_cast<T&&>(param);//万能引用
}

 With the basis of move, forward is not difficult to understand.

It uses remove_reference to distinguish whether the incoming parameter is an lvalue reference or an rvalue reference , and then calls the specific overloaded forward function.

Then, in the form of universal reference, return an lvalue reference or an rvalue reference according to the specific type of param.

The source code can be translated into the following form (int as an example):

template <typename T>
T&& forward(int& param)//左值引用
{
    return (T&&)(param);//万能引用
}

template <typename T>
T&& forward(int&& param)//右值引用
{
    return (T&&)(param);//万能引用
}

Reference article:

Talk about perfect forwarding in C++- Zhihu (zhihu.com)

C++ advanced knowledge: in-depth analysis of mobile constructors and their principles |

Reference books:

《C++ Primer》 

The program is my life, but I believe in loving her more than my life. —— unnamed


Please correct me if there is any mistake 

Guess you like

Origin blog.csdn.net/weixin_61857742/article/details/127877905