C++ new features exploration (13.6): re-exploration of rvalue references

Related blog posts:
Exploration of C++ New Features (13): R-value Reference (r-value ref) && Exploration of
C++ New Features (16): Move Constructor Move Construct
C++ New Features Exploration (13.5): R-value Reference
C++ New Features Exploring (13.6): Re-exploring rvalue references

Rvalue reference

  Rvalue references are one of the important features introduced by C++11 that are as famous as Lambda expressions. Its introduction solves a large number of historical problems in C++, eliminates additional overheads such as std::vector, std::string, and makes the function object container std::function possible.

Left value, pure right value of right value, dead value, right value

  To understand what rvalue references are all about, you must have a clear understanding of lvalues ​​and rvalues. The left value (lvalue, left value) , as the name implies, is the value on the left side of the assignment symbol. To be precise, an lvalue is a persistent object that still exists after an expression (not necessarily an assignment expression). Right value (rvalue, right value) , the value on the right, refers to a temporary object that no longer exists after the expression ends.   In C++11, in order to introduce a powerful rvalue reference, the concept of rvalue is further divided into: pure rvalue and dying value . A pure rvalue (prvalue, pure rvalue), a pure rvalue , is either a pure literal, such as 10, true; or an evaluation; the result is equivalent to a literal or an anonymous temporary object, such as 1+2. Temporary variables returned by non-reference, temporary variables generated by operation expressions, primitive literals, and Lambda expressions are all pure rvalues.
  
  

  

It should be noted that string literals are rvalues ​​only in classes, and lvalues ​​when they are in ordinary functions . E.g:

Insert picture description here
  The expiring value (xvalue, expiring value) is a concept proposed by C++11 to introduce rvalue references (so in traditional C++, pure rvalue and rvalue are the same concept), that is, it is about to be destroyed, but The value that can be moved.

The dead value may be a little difficult to understand. Let’s look at such code: In such code, as far as the traditional understanding is concerned, the return value temp of the function foo is created internally and then assigned to v. However, when v obtains this object , Will copy the entire temp, and then destroy the temp. If the temp is very large, this will cause a lot of extra overhead (this is the problem that traditional C++ has always been criticized). In the last line, v is an lvalue, and the value returned by foo() is an rvalue (also a pure rvalue). However, v can be captured by other variables, and the return value generated by foo() is used as a temporary value. Once copied by v, it will be destroyed immediately and cannot be obtained or modified. The dead value defines such a behavior: the temporary value can be recognized and at the same time can be moved. After C++11, the compiler did some work for us. The lvalue temp here will be converted into this implicit rvalue , which is equivalent to static_cast<std::vector &&>(temp), and then here The v will move the value returned locally by foo. This is the move semantics that we will mention later .
Insert picture description here
  
  

Rvalue references and lvalue references

  To get a dead value , you need to use an rvalue reference : T&&, where T is the type. The declaration of the rvalue reference allows the lifetime of this temporary value to be extended . As long as the variable is still alive, the dead value will continue to live.
  C++11 provides the std::move method to unconditionally convert an lvalue parameter to an rvalue. With it, we can easily obtain an rvalue temporary object , for example:
Insert picture description here

//小问学编程
#include<iostream>
#include<string>
using namespace std;

void reference(string& str)
{
    
    
	cout<<"左值"<<endl;
}

void reference(string&& str)
{
    
    
	cout<<"右值"<<endl;
}

int main()
{
    
    
	string lv1="string";//lv1是一个左值
	//string&& r1=lv1;//非法,右值引用不能引用左值
	string&& rv1=std::move(lv1);//合法,move可以将左值转移为右值
	cout<<rv1<<endl;//string

	const string& lv2=lv1+lv1;//合法,常量左值引用能够延长临时变量的生命周期
	//lv2+=“Test”;//非法,常量引用无法被改变
	cout<<lv2<<endl;//string,string

	string&& rv2=lv1+lv2;//合法,右值引用延长临时变量生命周期
	rv2+="Test";//合法,非常量引用能够修改临时变量
	cout<<rv2<<endl;//string,string,string,Test

	reference(rv2);//输出左值
	//rv2虽然引用了一个右值,但由于它是一个引用,所以rv2依然是一个左值。

	return 0;
}

  Note that there is a very interesting question left over from history. Let's look at the following code
Insert picture description here
  first : The first question, why is it not allowed to bind a non-constant reference to a non-lvalue? This is because there is a logical error in this approach:
Insert picture description here
  because int& cannot refer to a double type parameter, a temporary value must be generated to save the value of s, so when increase() modifies the temporary value, s itself does not exist after the call is completed modified.
  The second question is why constant references are allowed to bind to non-lvalues? The reason is simple, because Fortran requires it.

Move semantics

  Traditional C++ designed the concept of copy/copy for class objects through copy constructors and assignment operators. However, in order to realize the movement operation of resources, the caller must use the method of copying and then destructuring, otherwise the object needs to be moved by itself. Interface. Imagine that when you move, you move the things at home directly to the new home, instead of copying everything (rebuying) and putting it in the new home, and then throwing away (destroying) all the original things. This is very anti-human. One thing.
  Traditional C++ does not distinguish the concept of "moving" and "copying", resulting in a large amount of data copying, wasting time and space. The appearance of rvalue references just solves the confusion between these two concepts. For example: Insert picture description here
in the above code:
  1. First, two A objects will be constructed inside return_rvalue, and then the output of the two constructors will be obtained;
  2. Function After returning, a dead value is generated, which is referenced by A's move structure (A(A&&)), thereby extending the life cycle, and the pointer in this rvalue is obtained and saved in obj, and the pointer of the dead value is Set to nullptr to prevent this memory area from being destroyed.
  Thereby avoiding meaningless copy structure and enhancing performance.

Attach example code:

//小问学编程
#include <iostream>
class A
{
    
    
public:
    int *pointer;
    A():pointer(new int(1))
    {
    
    
        std::cout << " 构造" << pointer << std::endl;
    }
    A(A& a):pointer(new int(*a.pointer))
    {
    
    
        std::cout << " 拷贝" << pointer << std::endl;
    } // 无意义的对象拷贝
    A(A&& a):pointer(a.pointer)
    {
    
    
        a.pointer = nullptr;
        std::cout << " 移动" << pointer << std::endl;
    }
    ~A()
    {
    
    
        std::cout << " 析构" << pointer << std::endl;
        delete pointer;
    }
};
// 防止编译器优化
A return_rvalue(bool test)
{
    
    
    A a,b;
    if(test) return a; // 等价于static_cast<A&&>(a);
    else return b; // 等价于static_cast<A&&>(b);
}
int main()
{
    
    
    A obj = return_rvalue(false);
    std::cout << "obj:" << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
}

Let's take a look at an example involving the standard library:
Insert picture description here
attach the example code:

#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string

int main()
{
    
    
    std::string str = "Hello world.";
    std::vector<std::string> v;
    // 将使用push_back(const T&), 即产生拷贝行为
    v.push_back(str);
    // 将输出"str: Hello world."
    std::cout << "str: " << str << std::endl;
    // 将使用push_back(const T&&), 不会出现拷贝行为
    // 而整个字符串会被移动到vector 中,所以有时候std::move 会用来减少拷贝出现的开销
    // 这步操作后, str 中的值会变为空
    v.push_back(std::move(str));
    // 将输出"str: "
    std::cout << "str: " << str << std::endl;

    return 0;
}

Perfect forwarding

  As we mentioned earlier, a declared rvalue reference is actually an lvalue. This has caused problems for our parameter forwarding (passing):

Example: For pass(1), although it is an rvalue, since v is a reference, it is also an lvalue. Therefore reference(v) will call reference(int&) and output "left value" . For pass(l), l is an lvalue, why is it passed to pass(T&&) successfully?   This is based on the reference collapse rule : in traditional C++, we cannot continue to reference a reference type, but C++ relaxes this practice due to the appearance of rvalue references, which leads to the reference collapse rule, allowing us to reference For quoting, both left and right quotations can be used. But it follows the following rules:   Therefore, the use of T&& in the template function may not be able to carry out rvalue references. When an lvalue is passed in, the reference of this function will be deduced as an lvalue. More precisely, no matter what type of reference the template parameter is, if and only if the actual parameter type is a right reference, the template parameter can be deduced as a right reference type . This allows v to be successfully passed as an lvalue.Insert picture description here
  

Insert picture description here

Attach example code:

//小问学编程
#include <iostream>
using namespace std;

void reference(int& v)
{
    
    
    std::cout << " 左值" << std::endl;
}
void reference(int&& v)
{
    
    
    std::cout << " 右值" << std::endl;
}
template <typename T>
void pass(T&& v)
{
    
    
    std::cout << " 普通传参:";
    reference(v); // 始终调用reference(int&)
}
int main()
{
    
    
    std::cout << " 传递右值:" << std::endl;
    pass(1); // 1 是右值, 但输出是左值
    std::cout << " 传递左值:" << std::endl;
    int l = 1;
    pass(l); // l 是左值, 输出左值
    return 0;
}

  Perfect forwarding is based on the above-mentioned rules. The so-called perfect forwarding is to allow us to maintain the original parameter type when passing parameters (left reference remains left reference, right reference remains right reference). In order to solve this problem, we should use std::forward to forward (pass) the
Insert picture description here
  parameters : whether the passed parameters are lvalues ​​or rvalues, ordinary parameters will be forwarded as lvalues, so std::move will always An lvalue is received, and the reference(int&&) is then forwarded to output the rvalue reference.
  Only std::forward does not cause any redundant copy, and at the same time it perfectly forwards (transfers) the actual parameters of the function to other functions called internally.
  std::forward is the same as std::move. It doesn’t do anything. std::move simply converts the lvalue to the rvalue, and std::forward simply converts the parameters. See, std::forward(v) and static_cast<T&&>(v) are exactly the same.

Attach example code:

//小问学编程
#include <iostream>
#include <utility>
void reference(int& v)
{
    
    
	std::cout << "左值引用" << std::endl;
}
void reference(int&& v)
{
    
    
	std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v)
{
    
    
	std::cout << "             普通传参:";
	reference(v);
	std::cout << "       std::move 传参:";
	reference(std::move(v));
	std::cout <<"    std::forward 传参:";
	reference(std::forward<T>(v));
	std::cout <<"static_cast<T&&> 传参:";
	reference(static_cast<T&&>(v));
}
int main()
{
    
    
	std::cout << " 传递右值:" << std::endl;
	pass(1);
	std::cout << " 传递左值:" << std::endl;
	int v = 1;
	pass(v);
	return 0;
}

  Readers may be curious, why a statement can return the corresponding value for two types, let's take a look at the specific implementation mechanism of std::forward, std::forward contains two overloads:
Insert picture description here
  in this implementation, The function of std::remove_reference is to eliminate the reference in the type, and std::is_lvalue_reference is used to check whether the type deduction is correct. In the second implementation of std::forward, it is checked that the received value is indeed an lvalue, and then Reflects the rules of collapse.
  When std::forward accepts an lvalue, _Tp is deduced as an lvalue, and so the return value is an lvalue; when it accepts an rvalue, _Tp is deduced as an rvalue reference, based on the collapse rule, the return value becomes The right value of && + &&. It can be seen that the principle of std::forward is to cleverly use the difference in template type derivation.
  At this time, we can answer the question: Why is auto&& the safest way to use loop statements ? Because when auto is deduced as different left and right references, the collapsed combination with && is perfect forwarding.

Guess you like

Origin blog.csdn.net/weixin_43297891/article/details/113799702