In-depth understanding of rvalue references and move semantics

write in front

This article mainly sorts out the following issues for you:

  • What is an rvalue
  • The meaning and usage scenarios of rvalue references
  • std::moveThe nature of functions
  • How to write a move constructor
  • Universal citation and perfect forwarding

References:

  1. "Understand C++ rvalue references and std::move in one article"

  2. "Advanced Knowledge of C++: In-depth Analysis of the Move Constructor and Its Principles"

  3. 《Value categories》

  4. "C++11 value category and move semantics"

Due to the author’s limited knowledge and limited knowledge, everyone is welcome to correct me if I have a lack of understanding.


1. What is an rvalue and what is an lvalue?

 Each C++ expression (including operators and their operands, literal values, variable names, etc.) has two independent attributes: type and value category :
 Everyone is familiar with type (type), which refers to the data type of the expression. , which defines the value range and executable operations of the expression . For example, an integer expression may be of type int, and a floating-point expression may be of type float.
 Value category can be understood as the identity and portability of expressions . According to the C++ standard, there are three main value categories: rvalue, lvalue, and xvalue. The relationship between the three is as follows:
Insert image description here

  • The identity determines whether it has expression addressability, that is, whether we can get its address in memory
  • If mobility appears in assignment, initialization, etc. statements, will the statement exhibit move semantics?

The above is somewhat abstract. Let’s analyze it with specific examples and differentiate based on the characteristics:

[Rvalues ​​and rvalue references]:

// 以下都是常见的右值
10;
x + y;
fmin(x, y);

// 右值引用
int&& rr1 = 10;
double&& rr2 = x + y;             
double&& rr3 = fmin(x, y); 		  
  • 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.
  • Rvalues ​​cannot be placed to the left of the assignment symbol. If you don’t believe me, try it.10 = x + y
  • A reference to an rvalue is an rvalue reference, &&represented by
  • You cannot get an address from an rvalue. If you don’t believe me, try it.int* p = &10;

[Lvalue and lvalue reference]:

// 以下a、b、pa都是左值
int a = 10;    
int b = a;
int* pa = &a; 

// 左值引用
int& rla = a;
  • lvalues ​​can appear to the left and right of the assignment symbol
  • You can take its address and assign a value to it.

Let’s consider some difficult questions:

1.1 Can rvalue references refer to lvalues?

Can. std::move()Functions can coerce lvalues ​​into rvalues. That's right, it's forced type conversion . We will talk about it in detail later in conjunction with the source code:

int a = 10;
int&& rr = std::move(a);

1.2 Are lvalue references and rvalue references themselves lvalues ​​or rvalues?

 The declared left and right value references are all lvalues. Because according to the C++ language specification, whether they are lvalue references or rvalue references, they are considered named objects, have addresses and can be addressed . Therefore, when using references, they are treated as lvalues. Verify with the following code:

void check(int&& rr) {
    
    
	cout << "Yes" << endl;
}

int main() {
    
    
	int a = 5;	       		 // a是个左值
	int& ref_a_left = a;	   	// ref_a_left是个左值引用,本身是左值
	int&& ref_a_right = std::move(a); // ref_a_right是个右值引用,本身是左值

	check(a);              // 编译不过,无法将左值绑定到右值
	check(ref_a_left);     // 编译不过,左值引用ref_a_left本身也是个左值
	check(ref_a_right);    // 编译不过,右值引用ref_a_right本身也是个左值

	check(std::move(a));			// 编译通过
	check(std::move(ref_a_right));  // 编译通过
	check(std::move(ref_a_left));   // 编译通过
}

 The rvalue reference itself is an lvalue, so it is not difficult to understand why the rvalue cannot be modified, but the rvalue reference can be modified: we are not modifying the rvalue, but modifying the object referenced by the rvalue reference; rvalue A reference is an lvalue, which has its own identifier and address.

int&& rr = 10;
rr = 20;

1.3 Special const lvalue reference

 const lvalue references are special, they can accept both lvalues ​​and rvalues. Like rvalue references, const lvalue references can extend the rvalue 生命周期to avoid creating dangling references . Here are the instructions from cppreference:

Insert image description here

 When we bind an rvalue to a const lvalue reference, the compiler automatically creates a temporary object and binds the rvalue to the temporary object. The life cycle of this temporary object will be the same as the life cycle of the const lvalue reference , thus ensuring that this rvalue can be used safely within the scope of the const lvalue reference.
 This is one of the reasons why you often see const& used as function parameters. Without const, this code will not compile:v.push_back(5)

void push_back (const value_type& val);

2. The meaning of rvalue reference and move construction

 Lvalue references can improve efficiency both as parameters and return values. But the shortcoming of lvalue references is that if the referenced object is scoped, then lvalue references cannot be used. For example, in the following example (without considering compiler optimization), when the hello() function returns, it will first "hello world"copy to a temporary variable.
  This temporary variable essentially belongs to 将亡值, has identity and mobility at the same time . Before there is a move construct, s can only make another copy of the contents of the temporary variable, and watch helplessly as the temporary variable is destroyed when its life cycle is up - a waste!

string hello() {
    
    
	return "hello world";
}
string s = hello();

 But for an object that is about to be destroyed, why don't we be smarter and directly occupy the resources? Transferring ownership of the other party's resources is the core idea of ​​mobile construction. How to transfer? It can be intuitively understood as an exchange of pointers:

namespace my{
    
    
	string(string&& s)
	    :_str(nullptr) ,_size(0), _capacity(0){
    
    
        swap(str_, s.str_)        // 所有权转移 
	}
};

 Note that after move construction, the pointer in the original object must be set to null, otherwise a space will be deleted twice. And for nullptr, delete multiple times has no effect.

 Since move construction is so popular, let us further think about which resources can be moved and constructed? The designers of C++ noted that in most cases, objects contained in rvalues ​​can be moved safely .

 So the question is, how do we explicitly tell the compiler that we want to receive an rvalue? Before C++11, only const-modified lvalue references could receive rvalues . This was a big problem. You were modified by const, so how could I "steal" your resources? Following this logical thinking, the emergence of rvalue references is also inevitable, and it spreads fertile soil for the emergence of mobile structures.

 Next we will manually implement the move constructor

3. Use of move constructor

namespace my {
    
    
	class string {
    
    
	public:
		string() : len_(0), cap_(0), data_(nullptr) {
    
    }

		string(const char* s) {
    
    
			// 略
		}

		string(const my::string& s) {
    
    
			cout << "拷贝构造函数" << endl;
			// 略
		}
		
		my::string& operator=(const my::string& s) noexcept {
    
    
			cout << "赋值运算符重载" << endl;
			// 略
		}
		
		string(my::string&& s) {
    
    
			cout << "移动构造函数" << endl;
			len_ = s.len_;
			cap_ = s.cap_;
			swap(data_, s.data_);
		}

		my::string& operator=(my::string&& s) noexcept{
    
    
			cout << "移动赋值运算符重载" << endl;
			len_ = s.len_;
			cap_ = s.cap_;
			if (data_) {
    
    
				delete data_;
				data_ = nullptr;
			}
			swap(data_, s.data_);
			return *this;
		}

	private:
		char* data_;
		int len_;
		int cap_;
	};
};

STL libraries basically support move construction and move assignment, such as string, etc.

string (string&& str) noexcept;
string& operator= (string&& str) noexcept;

The swap function is also
Insert image description here
 to share with you a mistake I easily made when I was new: We still use the my::string class implemented above for testing. Do you think there is s = t.sany move assignment called in?

class test {
    
    
	public:
		test() {
    
    }
		// ……
		test(test&& t) {
    
    
			s = t.s;
		}

	private:
		my::string s;
	};

Insert image description here
The answer is not. Although t is an rvalue, tm is indeed an lvalue:
Insert image description here

4. Implementation principle of move

 When you first start learning std::move, it is always easy for everyone to misunderstand the move function, thinking that the move function completes the movement of resources on the memory. However, in fact, the work completed by move is just forced type conversion . Let’s take a look at the corresponding source code:

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

 Although we don't know the details, we can roughly see that move completes the work of forced type conversion. For a more thorough understanding, I will explain the details for you.

  • In the template, &&it does not represent an rvalue reference, but a universal reference . It can receive both lvalues ​​and rvalues.

  • What does this return value typename remove_reference<T>::type&&mean? type is a type member defined in remove_reference, so when accessing it, use :: to access it the same way as accessing static members. The class is a template class, so the typename keyword must be added in front of it.

  • remove_referenceAs can be seen from its name, it removes references through templates
    Insert image description here

  • We assume that T is int&, that is, an lvalue is passed in, then the above code can finally be simplified into the following form:

    int && move(int& && t){
          
          
        return static_case<int&&>(t);
    }
    
  • When encountering int& &&, reference folding will occur. The folding rules are as shown in the figure below:
    Insert image description here

  • So in the end move actually does this:

    int && move(int& t){
          
          
        return static_case<int&&>(t);
    }
    

5. Perfect forwarding

 We mentioned earlier that in a template, &&you can receive both lvalues ​​and rvalues, but when we pass val to another function inside a function, val will degenerate. At this time, val is always passed as an lvalue .

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 = 10;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
}

Insert image description here

  • In order to maintain the original left and right value attributes of the parameters, we need to use std::forward<模板参数>()functions to achieve perfect forwarding:
    Insert image description here

  • Notice! In order to maintain the original left and right value attributes of the parameters, all downward forwarding needs to achieve perfect forwarding:

Guess you like

Origin blog.csdn.net/whc18858/article/details/132918508