当前位置:网站首页>[CPP] lvalue, rvalue and STD:: move

[CPP] lvalue, rvalue and STD:: move

2020-11-10 17:23:26 zininben

Reference article :

brush Leetcode when , From time to time I come across the following 2 Kind of traversal STL How to write a container :

int main()
{
    vector<int> v = {1, 2, 3, 4};
    for (auto &x: v)
        cout<<x<<' ';
    cout<<endl;
    for (auto &&x: v)
        cout<<x<<' ';
    cout<<endl;
}

One problem that has bothered me for a long time is auto & and auto && What's the difference? ?

The left value 、 Right value 、 Pure right value 、 Dead value

First of all, we need to define a concept , value (Value) And variables (Variable) It's not the same thing :

  • Only value Category (category) Division , The variables are only type (type) Division .
  • Value doesn't necessarily have identity (identity), You don't have to have a variable name ( for example Expression intermediate result i + j + k).

Definition

The left value (lvalue, left value), As the name implies, the value to the left of the assignment symbol . To be precise , The lvalue is the expression ( It doesn't have to be an assignment expression ) Persistent objects that still exist after .

Right value (rvalue, right value), The value on the right , A temporary object that no longer exists after the end of an expression .

C++11 In order to introduce a powerful right value reference , The concept of right value is further divided , It is divided into : Pure right value and will die value .

Pure right value (prvalue, pure rvalue), Pure right value , Or it's just literal , for example 10, true; Either the evaluation result is equivalent to literal or anonymous temporary object , for example 1+2. Temporary variable returned by non reference 、 A temporary variable generated by an operational expression 、 The original literal amount 、Lambda Expressions are all pure right values .

C++( Include C ) All expressions and variables in are either lvalues , Or right value . The popular definition of lvalue is non temporary object , Objects that can be used in multiple statements . All variables satisfy this definition , It can be used in more than one code , It's all l-values . The right value is a temporary object , They are only valid in the current statement .

Example :

int i = 0; // ok, i is lvalue, 0 is rval

//  The right value can also appear on the left side of the assignment expression ,  But it can't be used as the object of assignment , Because the right value is only valid in the current statement , Assignment doesn't make sense .
// 0  As a right value, it appears in ”=” Left side . But the assignment object is  i  perhaps  j, It's all l-values .
(i > 0? i : j) = 233

summary :

  • All variables It's all l-values .
  • Right values are temporary , There is no such thing after the end of the expression , Count now 、 Expression intermediate result It's all right values .

A special case

It should be noted that , String literals are right values only in classes , When it's in a normal function, it's an lvalue . for example :

class Foo
{
    const char *&&right = "this is a rvalue"; //  Here the literal value of the string is the right value 
    // const char *&right = "hello world";    // error
public:
    void bar()
    {
        right = "still rvalue"; //  Here the literal value of the string is the right value 
    }
};
int main()
{
    const char *const &left = "this is an lvalue"; //  Here the literal value of the string is l-value 
    // left = "123"; // error
}

Dead value

Dead value (xvalue, expiring value), yes C++11 A concept proposed to introduce right-hand reference ( So in tradition C++ in , Pure right value and right value are the same concept ), It's about to be destroyed 、 But the value that can be moved . The dead value expression , namely :

  • Returns the calling expression of the function referenced by the right value
  • The call expression of the conversion function converted to an R-value reference , for example move

Let's look at an example :

vector<int> foo()
{
    vector<int> v = {1,2,3,4,5};
    return v;
}
auto v1 = foo();

According to tradition C++ The way ( It's all of us C++ Rookie's understanding ), The execution mode of the above code is :foo() Create and return a temporary object inside the function v , And then execute vector<int> Copy constructor for , complete v1 The initialization , Finally, foo The temporary objects in the .

that , At some point , There is a 2 Share the same vector data . If the object is large , It's going to cost a lot of extra money .

stay v1 = foo() in ,v1 It's a left value , Can be continued to use , but foo() It's a pure right value , foo() The generated return value is used as a temporary value , One Dan Bei v1 After copying , Will be destroyed immediately , Can't get 、 It can't be modified .

The death value defines such a behavior : Temporary values can be identified 、 At the same time, it can be moved .

stay C++11 after , The compiler does some work for us ,foo() The internal left value v Will be carried out Implicit right value conversion , Equivalent to static_cast<vector<int> &&>(v), And then here v1 Will foo Move the value returned locally . This is the mobile semantics that will be mentioned later std::move() .

The personal understanding is , This kind of grammar is introduced to realize and Java A similar object reference system in .

L-value reference and right value reference

Examples of distinguishing between an lvalue reference and an rvalue reference

Let's start with a piece of code :

int a;  
a = 2;  //a It's left ,2 It's right value 
a = 3;  // The lvalue can be changed , Compile and pass 
2 = 3;  // The right value cannot be changed , error 

int b = 3;  
int* pb = &b;  //pb It's left ,&b It's right value , Because it's the value returned by the addressing operator 
&b = 0;  // error , The right value cannot be changed 

// lvalues:
int i = 42;
i = 43; // ok, i is an lvalue 
int* p = &i; // ok, i is an lvalue 
int& foo();
foo() = 42; // ok, foo() is an lvalue
int* p1 = &foo(); // ok, foo() is an lvalue
// rvalues: 
int foobar(); 
int j = 0;
j = foobar(); // ok, foobar() is an rvalue
int k = j + 2; // ok, j+2 is an rvalue
int* p2 = &foobar(); // error, cannot take the address of an rvalue 
j = 42; // ok, 42 is an rvalue

So here comes the question : Whether the return value of the function will only be the right value ? Of course not. .

vector<int> v(10, 0);
v[0] = 111;

obviously ,v[0] Will execute [] The symbol overloaded function of int& operator[](const int x) , Therefore, the return value of the function may also be an l-value .

Explain profound theories in simple language

To get a dying value , You need to use the right-hand reference T &&, among T It's the type . The declaration of the right-hand reference extends the lifetime of the temporary value , As long as the variable is alive , Then the death value will continue to survive .

C++11 Provides std::move This method Convert the l-value parameter to the right value unconditionally , With it, we can easily get an R-value temporary object , for example :

#include <iostream>
#include <string>
using namespace std;
void reference(string &str) { cout << "lvalue ref" << endl; }
void reference(string &&str) { cout << "rvalue ref" << endl; }
int main()
{
    string lv1 = "string,"; // lv1 is lvalue
    // string &&r1 = lv1;  //  illegal , Right value references cannot refer to lvalues 
    string &&rv1 = std::move(lv1); //  legal ,move  The left value can be transferred to the right value 
    cout << rv1 << endl;

    // string &lv2 = lv1 + lv1; //  illegal , The initial value of the non constant reference must be the left value 
    const string &lv2 = lv1 + lv1; //  legal , Constant lvalue references can extend the lifetime of temporary variables 
    cout << lv2 << endl;

    string &&rv2 = lv1 + lv2; //  legal , Right value references extend the lifetime of temporary objects ( adopt  rvalue reference  quote  rval)
    rv2 += "Test";
    cout << rv2 << endl;

    reference(rv2); //  Output  "lvalue ref"
    // rv2  Although a right value is quoted , But because it's a reference , therefore  rv2  It's still an l-value .
    //  in other words ,T&& Doesn’t Always Mean “Rvalue Reference”,  It can bind lvalues , You can also bind right values 
}

Why not allow non constant references to be bound to lvalues ?

One explanation is as follows (C++ What a fool ).

This question is equivalent to explaining the following code :

int i = 233;
int &r0 = i; // ok
double &r1 = i; // error
const double &r3 = i; // ok

because double &r1 The type and int i Mismatch , So no , What then? const double &r3 = i Yes. ? Because it's actually equivalent to :

const double t = (double)i;
const double &r3 = t;

stay C++ in , All temporary variables are const Type of , So there was no const No way. .

Mobile semantics

Let's start with a piece of code , Be familiar with it. move What did you do :

#include <iostream>
#include <string>
using namespace std;
int main()
{
    string a = "sinkinben";
    string b = move(a);
    cout << "a = \"" << a << "\"" << endl;
    cout << "b = \"" << b << "\"" << endl;
}
// Output
// a = ""
// b = "sinkinben"

Then read the following code , End the round .

template <class T> swap(T& a, T& b){
  T tmp(a);  // Two copies are available a A copy of the ,tmp and a
  a = b;     // Two copies are available b A copy of the ,a and b
  b = tmp;   // Two copies are available tmp A copy of the ,b and tmp
}

// Try a better way , No extra copies will be generated 
template <class T> swap(T& a, T& b){
  T tmp(std::move(a)); // There is only one copy ,tmp
  a = std::move(b);    // There is only one copy ,a
  b = std::move(tmp);  // There is only one copy ,b
}

Personal feeling ,b = move(a) This semantic operation , yes Put variables b Bind to data a On the memory area of , This avoids meaningless data copy operations .

The following code can confirm my point of view .

#include <iostream>
class A
{
public:
    int *pointer;
    A() : pointer(new int(1))
    {
        std::cout << " structure " << pointer << std::endl;
    }
    A(A &a) : pointer(new int(*a.pointer))
    {
        std::cout << " Copy " << pointer << std::endl;
    } //  Meaningless copy of objects 
    A(A &&a) : pointer(a.pointer)
    {
        a.pointer = nullptr;
        std::cout << " Move " << pointer << std::endl;
    }
    ~A()
    {
        std::cout << " destructor " << pointer << std::endl;
        delete pointer;
    }
};
//  Prevent compiler optimization 
A return_rvalue(bool test)
{
    A a, b;
    if (test)
        return a; //  Equivalent to  static_cast<A&&>(a);
    else
        return b; //  Equivalent to  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;
}
/* Output
 structure 0x7f8477405800
 structure 0x7f8477405810
 Move 0x7f8477405810
 destructor 0x0
 destructor 0x7f8477405800
obj:
0x7f8477405810
1
 destructor 0x7f8477405810
*/

about queue perhaps vector, We can also pass move Improve performance :

// q is a queue
auto x = std::move(q.front());
q.pop();
// v is a vertor
v.push_back(std::move(x));

If STL The elements in 「 Volume 」 It's big , It also saves a little bit of money , Improve performance .

Perfect forwarding

To tell you the truth , This translation is a spicy chicken . English name Perfect Forwarding .

This is to solve such a problem : Arguments are passed into the function , When it's passed to another function , It's still a left or right value .

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T t){ 
    cout<<"f1"<<endl;
    f2(t);  
    // If t It's right value , We want to introduce f2 It's also the right value ; If t It's left , We want to introduce f2 It's also a left value 
}   
// stay main In the function :
int a = 2;
f1(3); // Pass in the right value 
f1(a); // Pass in the lvalue 

Before the introduction of balabalabala's mechanism , namely C++11 What happened before ? When we are from f1 call f2 When , No matter what comes in f1 Is it right or left , because t It's a variable name , Pass in f2 It's all left-handed , This will result in a call to T It is a waste of resources to generate unnecessary copies .

So now there's one called forward Function of , You can do this :

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T&& t) {    // This is a generic reference , Instead of the right-hand reference 
    cout<"f1"<<endl;
    f2(std::forward<T>(t));  //std::forward<T>(t) Used to handle t Forward to left or right value , It depends on T
}

such ,f1 call f2 When , That's what's called move constructor Instead of copy constructors , You can avoid unnecessary copying , This is called 「 Perfect forwarding 」.

Perfect forwarding , Fool home .

Conclusion

The question raised at the beginning of this article auto & and auto && What's the difference? ? The problem is more complicated , involves Universal Reference The concept , You can refer to this 2 An article :

Let's talk about it later .

Idiot C++ .

版权声明
本文为[zininben]所创,转载请带上原文链接,感谢