C++, rvalue reference

1. Lvalues ​​and rvalues

Lvalues ​​and rvalues ​​usually correspond to the left and right sides of the assignment symbols =, but = The right side can also be an lvalue. An lvalue refers to an addressable named variable, which means that we can obtain its storage address at any time during its lifetime and scope, and read or modify its value through its name. Rvalues ​​are usually unaddressable and about to die. Once they leave the location where they appear, they will be destroyed and can no longer be addressed, such as some literal constants, temporary variables, etc.

In the past C++ standard, we could make an lvalue reference, that is, add an alias to a named variable. As long as the lifetime of the variable has not ended, we can use this alias to access and access data. Revise. However, rvalues ​​are about to die, and will be destroyed once they leave the position where they appear, that is, their lifespan is over. Therefore, generally speaking, it is meaningless to add an alias to reference them, so it was not possible in the past standards. Takes a reference to an rvalue (except a const reference). However, due to the development of engineering project applications, the data volume of some class objects is increasing, such as strings and image data, etc. In scenarios where deep copy of objects must be performed, if there are too many temporary objects (rvalues), perform Deep copy will add too much extra burden. For example, when a function returns by value, the return value will be copied to a temporary variable first, and then the temporary variable will be copied to the target variable. So in C++11, the mechanism of rvalue references was introduced. To distinguish lvalue references &, rvalue references use && Symbol.

int x = 1;     	// x是左值,在其生命期中我们可以通过名字进行读取和修改
               	// 1是右值,当其赋值给x以后立即被销毁
A a = A();     	// 假设A是一个类,那么a也是左值,而A()为右值,属于临时变量
A &b = a;      	// 左值引用,等号右侧必须为左值,相当于给左值a取了一个别名
A &c = A();    	// 错误,因为A()属于临时变量,无法寻址,所以不能进行左值引用
A &&d = A();   	// 右值引用,相当于为临时变量A()取了一个名字,之后我们可以通过d访问A()的内容
int &y = x + 1;	// 错误,x+1也是一个临时变量,所以不能用于左值引用
int &&z = x;   	// 错误,右值引用不能绑定到左值
int &&z = std::move(x); // move()可以将输入的变量强制转换为右值,但没有发生内存的拷贝

An rvalue reference is equivalent to creating a name for the rvalue, so that subsequent code can access and modify the data based on this name. This will inevitably require the life of the data as an rvalue to be extended. , it will not be destroyed immediately after leaving the location where it appeared like before. Indeed, an rvalue reference will extend the lifetime of the rvalue to the lifetime of an ordinary local variable in its scope. For example, in the above exampleA &&d = A(); the temporary object A()< /span>a itself is being destroyed. A () that is destroyed, but the original rvalued is in the same scope. Of course, the specific destruction order is also based on the stack order during construction. Note that during destruction, it is not the rvalue reference object because it has the same lifetime as a has the same lifetime as the A a = A(); will be destructed immediately after the semicolon ends, but due to rvalue references, it has the extension to and

Note that variables should not be easily assigned to other variables after they are converted to rvalues ​​through move(). Although this generally has no impact on ordinary types, for class types that include move constructors and move assignment functions, rvalue attributes may It causes it to lose ownership of the internal dynamic memory. For details, please refer to the later chapters such as move semantics. In the following example, because string contains a move assignment function, after x1 is converted to an rvalue, the assignment will cause a dynamic memory ownership transfer, and x1 becomes an empty string. Although x2 is an rvalue reference, because it has a name, x2 is also an lvalue. The assignment will not cause a dynamic memory ownership transfer, and the string content of x2 remains unchanged.

string   x1 = "abc";
string &&x2 = "abc";
string    y = "";
y = move(x1);  // x1 = "", y = "abc"
y = x2;        // x2 = "abc", y = "abc"

Rvalue references can be used to solve problems with moving semantics and perfect forwarding.

2. rvalue reference

For functions, the two commonly used parameter passing methods are passing by value and lvalue reference, as follows:

void fn1(A a);
void fn2(A &a);

If passed by value, the actual parameters will be copied first and then passed into the function. Therefore, if the memory occupied by A is large, the efficiency of parameter passing will be reduced. With an lvalue reference, we cannot pass in a temporary variable, i.e. fn2(A()) is wrong because A() is an rvalue. In order to solve the rvalue problem, we can define void fn3(const A &a); because constant references can use rvalues, but then we a cannot be modified in the function body, so we can consider overloading fn2 and fn3. The compiler will handle lvalues ​​and rvalues ​​respectively according to the type of the actual parameters passed in.

However, when there is more than one parameter, we need exponential function overloading to handle these parameters separately. To this end, C++11 introduces rvalue references. We can define void fn(A &&a); but at this time the function The input must be an rvalue, that is

fn(A());    // 正确,A()是右值
A a = A();  // 或者 A &&a = A();
fn(a);      // 错误,a是左值,即便a本身是右值引用的,因为a是具名的
fn(std::move(a));    // 正确,通过move函数转换为右值

To avoid explicit rvalue conversion using move(), template functions are generally used because template functions automatically perform type deduction and folding. Type folding: T&& && is equivalent to T&&, and other T& &&, T&& &, T& & are equivalent to T&. Examples are as follows:

template <typename T>
void fn(T&& a);
int a = 1;
fn(a);  // 实际为fn(int& &&),折叠后为fn(int &),也就是左值引用
fn(std::move(a)); // 实际为fn(int&& &&),折叠后为fn(int &&),也就是右值引用

Through rvalue references and type folding, we can use a function to handle the parameter reference passing problem of lvalues ​​and rvalues. The following is an example of writing a copy constructor using rvalue references and type folding.

class A 
{
    
    
public:
	A(int _x = 0) :x(_x) {
    
    }
	~A() {
    
     cout << this << ' ' << x << endl; }
	template <typename T>
	A(T&& a) {
    
     x = a.x;  a.x++; }
private:
	int x;
};

int main()
{
    
    
	A a(5);		// 调用了 A(int); 构造函数
	A b(a);		// 调用了模板实例化为 A(A &); 的拷贝构造函数 
	A c(A(1));	// 调用了模板实例化为 A(A &&); 的拷贝构造函数
	return 0;
}

In fact, the role of rvalue references is not limited to saving the number of function overloads through type folding. Its more general use lies in the move semantics mentioned later a> and perfectly forwarded. In the example of type folding, we want to unify the behavior of lvalue input and rvalue input, that is, only one function can receive lvalue or rvalue input at the same time, and lvalue input and rvalue input are in the function The operations in are completely consistent. However, in some application scenarios of object copying, we can find that this approach is relatively inefficient.

3. Move semantics

Take string as an example, if you want to add two string objectsoperator+(s1, s2), because this is not a>operator+=, if s1 or s2 are lvalues, they may be referenced by other variables, or be referenced by other variables later, then we The values ​​of these two string objects should not be modified, so the input type is generally const string&, and a new string object is constructed to copy the contents of the two and return this string object. Note that because we return a string created within the function, we generally use return by value and use friend functions, that is

friend string operator+(const string &s1, const string &s2);

Because constant references are used as input, this also works for input of rvalue string objects. Although you can also choose to new a string object and then return a pointer, function callers often forget to delete when they no longer need to use the object, causing memory leaks. Of course, this problem can also be solved with smart pointers.

Look againstring s = string(“a”) + string(“b”); This process is implemented according to the above , we first constructed the string objects a="a", b="b", and then copied the strings "a" and "b" respectively within the function to form a new string "ab", and constructed it based on this string The new string object ab = "ab". Due to the return by value, the object ab will be copied to a temporary string object tmp, and then copied to the target string object s. Even if it is returned through a pointer, the overhead of this process is still Relatively large.

The main problem above is that if s1 and s2 are both rvalues, then modifying s1 or s2 will not affect subsequent programs. This is because rvalues ​​are unaddressable and will expire, which means they will no longer be referenced by other variables now or in the future. Therefore, we can directly insert the string data of s2 to the end of the string data of s1, and then return the s1 object. In this process, we save the character data copy of the s1 object and the construction of the ab object (but two copies are still required when returning). In comparison, the efficiency is obviously higher.

In this way, we have introduced the concept of movement semantics. In the traditional implementation, an object a is an object a, and an object b is an object b. Their semantics (or content) will not change due to function calls. After the concept of rvalue is introduced, the a object itself is an rvalue and will be destroyed immediately after the function call. Changing its content will not have any impact, so we can change its content to the content of the ab object. , thereby omitting the construction of the ab object and the additional (string data of the s1 object itself) copy operation required during construction, that is, the semantics of the a object are transferred to the semantics of the ab object. In the process the efficiency of the functions we implement increases, which is why we need move semantics.

Above we discussed the move semantics used when adding two rvalue strings, but in fact the copy of an rvalue string can also use move semantics, which is often seen in some standard libraries. Move copy constructor and Move assignment function. Taking string as an example, if we need to copy an rvalue string object, we only need to assign the string pointer of the rvalue string object to the string pointer of the target string object without any real occurrence. Character copy (but the data members of the string object itself still need to be copied, but the time required is very small compared to the potentially long string data copy). But it should be noted that we must also remember to set the string pointer of the rvalue string object to null, so as to prevent the memory pointed to by the string pointer from being released when the rvalue string object is destroyed.

The following is an example of copying and adding rvalue string objects based on move semantics:

// 移动拷贝构造函数
Str::Str(Str &&right) noexcept
{
    
    
    len = right.len;
    buf = right.buf;
    right.len = 0;
    right.buf = NULL;
}
// 移动相加函数
Str operator+(Str &&left, Str &&right)
{
    
    
	// 注意left的字符串存储空间需要能够容纳right的字符串
    strcpy(left.buf + left.len, right.buf);
    left.len += right.len;
    return move(left);
}

Based on the above implementation, when executingStr s = Str(“a”) + Str(“b”);, due to Str ("a") and Str("b") are both rvalues, so the moving addition function we defined will be called. At this time, the string "b" will be directly copied to the end of the string "a" to form String "ab". In this process, we do not need to copy the string "a", nor do we need to construct a new string object Str("ab") for the string "ab". Note that when the operator+ function returns the semantically transferred object left, it must be re-converted to an rvalue through the move() function, because the name left means left is an lvalue, but the semantics expressed by left come from rvalues. After converting left into an rvalue through the move() function, the operator+ function returns an rvalue, and then the function we defined will be called Move constructorStr::Str(Str &&right), no real string data copy occurs in this process, so the rvalue Copying objects is very efficient.

We mentioned earlier that when the left object is returned, it will first be copied to a temporary object tmp, and then the tmp object will be copied to the target object s, that is, two copy operations occur. In this process, because move(left) is an rvalue, the move copy constructor is called when copying to the temporary object tmp; similarly, the temporary variable tmp itself is also an rvalue, so when tmp is copied to the target object s, the move copy constructor is also called. Is the move copy constructor. Therefore, although two copy constructions occur here, no copy of the string data itself occurs. From here we can see the efficiency improvement that move semantics can bring to object copying. This is why we need rvalue references and move semantics.

Note, try not to use an rvalue reference as the return of this function, because although the lifetime of the rvalue is extended, it cannot exceed its lifetime as an ordinary local variable, and the lifetime of an ordinary local variable ends when it leaves its scope. Finish. In the following example, Str("a") and Str("b") do not belong to the same scope as the target object a1, but are in a nested scope separated by the semicolon of this statement. When leaving When the semicolon ends this statement, both Str("a") and Str("b") will be destructed, and the memory they occupy will also be released. At this time, a1 refers to the released memory. Therefore, if a1 is operated and manually destructed later (references will not be destructed, but generally rvalue objects will be destructed automatically, and manual destruction is not required), it may cause a memory access error and cause a crash. . However, if you directly call the move constructor after returning to construct a new object such as a2, since the construction of a2 also occurs in the nested scope, the temporary variable has not been destructed at this time, this kind of copy is legal, and the copy After completion, a2 is an independent object, the data pointer of the temporary variable has been set to empty, and the destruction will not affect the subsequent operations of a2. In the same way, if you return a local variable inside a function, the same problem exists, and even its life span is shorter. When the function returns, the local variable has been released, and the variable cannot be retained even if a new object is moved and constructed. Content.

Str &&operator+(Str &&left, Str &&right)
{
    
    
    strcpy(left.buf + left.len, right.buf);
    left.len += right.len;
    return move(left);
}
Str &&a1 = Str("a") + Str("b"); // 危险,表达式结束后a.buf指向已经释放的内存
a1.~Str();                      // 错误,删除已经释放的内存,程序崩溃
Str a2 = Str("a") + Str("b");   // ok,此时调用移动构造函数复制临时对象

4. Perfect forwarding

Perfect forwarding is mainly used in conjunction with move semantics, assuming there are constant lvalue references and right Value refers to two versions of overloaded functions. We usually merge and encapsulate these two functions through type folding, thereby providing a unified calling interface function, for example

void fn(const int& a);
void fn(int&& a);

template<class T>
void pack(T&& x) {
    
    
    //fn(x);                // 实际上始终都会调用fn(const int&),因为x此时是具名的,所以属于左值
    fn(std::forward<T>(x)); // 通过forward函数,如果x是左值引用,那么会转换为左值;如果是右值引用会转换为右值
}

In the above example, if theforward() function is not used, because x itself has a name, x is an lvalue, regardless of When calling pack(x), whether the parameter passed in is an lvalue or an rvalue, then fn(const int&) will always be called, that is, lvalue The version of the constant reference. The forward() function is more intelligent than the move() function. Inputs to rvalue references are converted to rvalues, while inputs to lvalue references remain lvalues, choosing the appropriate overloaded function. In this process, we have completed the perfect forwarding of lvalue or rvalue input by the interface function.

Guess you like

Origin blog.csdn.net/qq_33552519/article/details/128869823