Left value reference and right value reference

Posted by daz_effect on Wed, 15 Dec 2021 04:33:22 +0100

#include <iostream>
using namespace std;
void change(int &rnum)//A reference is an alias for a variable name
{
   rnum =111;
}
//Do not use pointers where references can be used in c + +
int main()
{
	int num(10);//Lvalue, memory entity
	int &rnum(num);//Alias of variable
	rnum =1;//Alias equal to num
	cout << num<<endl;
	change(num);
    cout << num<<endl;
    cout << "Hello World";
    return 0;
}

void show(int &&rrnum)//rvalue reference 
{
    cout << rrnum<<endl; //6
}
	int main()
{
	int a[5]{1,2,3,4,5};
	show(a[3]+2);
    show(std::move(a[3]));//move moves the semantics, changing the left value into the right value
	cout << "Hello World";
    return 0;			
}

int main()
{
    int num(10);// Lvalue, memory entity
    int data =0;
    data = num+1;// Right value
    cout << data<<endl;
    cout <<(void*)&data<<endl;
    cout << "Hello World";
    return 0;
}

int main()
{
    int num(10);// Lvalue, memory entity
    int data1 = num+4;
    int &rdata(data1);// lvalue reference
    int &&data (num+4);// R-value reference, fast backup, compiler automatic recycling, saving memory
    
    printf("%p",&data);
    cout <<endl;
    cout << data<<endl;
    //cout <<(void*)&data<<endl;
    cout << "Hello World";
    return 0;
}

The difference between lvalue reference and lvalue reference:

Lvalue references often refer to values in memory

The right value reference uses the value in the register

lvalue reference

Let's first look at the traditional lvalue reference.

int a = 10;
int &b = a;  // Define an lvalue reference variable
b = 20;      // Modify the value of reference memory through lvalue reference

Lvalue references are actually the same as ordinary pointers at the assembly level; To define a reference variable, you must initialize it, because a reference is actually an alias. You need to tell the compiler whose reference is defined.

int &var = 10;

The above code cannot be compiled because 10 cannot perform address fetching operation and cannot fetch the address of an immediate, because the immediate is not stored in memory but in a register, which can be solved by the following methods:

const int &var = 10;

The constant reference is used to refer to the constant number 10. At this moment, a temporary variable is generated in memory and saved with 10. This temporary variable can be used for address fetching. Therefore, var actually refers to this temporary variable, which is equivalent to the following operations:

const int temp = 10; 
const int &var = temp;

Based on the above analysis, the following conclusions are drawn:

  • Lvalue reference requires that the value on the right must be able to get the address. If the address cannot be obtained, you can use constant reference;
    However, after using constant references, we can only read data through references and cannot modify data because it is modified into constant references by const.

Then C++11 introduces the concept of right value reference. Using right value reference can solve this problem well.

rvalue reference

C + + has no standard definitions for left and right values, but there is a widely accepted statement:

  • Can take the address, have a name, non temporary is the lvalue;
  • If you can't get an address, if you don't have a name, the temporary one is the right value;

It can be seen that the immediate value and the value returned by the function are all right values; Instead of anonymous objects (including variables), references returned by functions and const objects are lvalues.

In essence, the creation and destruction are controlled by the compiler behind the scenes. Programmers can only ensure that the right value (including immediate value) is valid in this line of code; The left value (including the reference of the local variable returned by the function and the const object) is created by the user and its lifetime can be known through the scope rules.

The format of defining the right value reference is as follows:

type && Reference name = R-value expression;

Right value reference is a new feature of C++ 11, so the reference of C++ 98 is left value reference. The right value reference is used to bind to the right value. After binding to the right value, the lifetime of the right value that would have been destroyed will be extended to the lifetime of the right value reference bound to it.

int &&var = 10;

At the assembly level, right-value references do the same thing as regular references, that is, generate temporary quantities to store constants. However, the only difference is that right value references can be read and written, while regular references can only be read and written.

The existence of right value reference is not to replace left value reference, but to make full use of the construction of right value (especially temporary object) to reduce object construction and deconstruction operations, so as to improve efficiency.

A simple sequential stack is implemented in C + +:

class Stack
{
public:
    // structure
    Stack(int size = 1000) 
	:msize(size), mtop(0)
    {
	cout << "Stack(int)" << endl;
	mpstack = new int[size];
    }
	
    // Deconstruction
    ~Stack()
    {
	cout << "~Stack()" << endl;
	delete[]mpstack;
	mpstack = nullptr;
    }
	
    // copy construction 
    Stack(const Stack &src)
	:msize(src.msize), mtop(src.mtop)
    {
	cout << "Stack(const Stack&)" << endl;
	mpstack = new int[src.msize];
	for (int i = 0; i < mtop; ++i) {
	    mpstack[i] = src.mpstack[i];
	}
    }
	
    // Assignment overload
    Stack& operator=(const Stack &src)
    {
	cout << "operator=" << endl;
	if (this == &src)
     	    return *this;

	delete[]mpstack;

	msize = src.msize;
	mtop = src.mtop;
	mpstack = new int[src.msize];
	for (int i = 0; i < mtop; ++i) {
	    mpstack[i] = src.mpstack[i];
	}
	return *this;
    }

    int getSize() 
    {
	return msize;
    }
private:
    int *mpstack;
    int mtop;
    int msize;
};

Stack GetStack(Stack &stack)
{
    Stack tmp(stack.getSize());
    return tmp;
}

int main()
{
    Stack s;
    s = GetStack(s);
    return 0;
}

The operation results are as follows:

Stack(int)             // Structure s
Stack(int)             // Construct tmp
Stack(const Stack&)    // tmp copy constructs temporary objects on the main function stack frame
~Stack()               // tmp deconstruction
operator=              // Temporary object assigned to s
~Stack()               // Temporary object destructor
~Stack()               // s deconstruction

In order to solve the problem of shallow copy, a custom copy constructor and assignment operator overload function are provided for the class, and the internal implementation of these two functions is very time-consuming and resource-consuming (first open up a large space, and then copy the data one by one). Through the above operation results, we found that two places use copy construction and assignment overload, tmp copies the temporary object on the main function stack frame and assigns the temporary object to s. both tmp and temporary objects are destroyed after their respective operations, which makes the program very inefficient.

In order to improve efficiency, can we directly give the memory resources held by tmp to temporary objects? Can I give the resources of temporary objects directly to s?

In C++11, we can solve the above problems by providing copy constructors with R-value reference parameters and overloaded functions of assignment operators

// Copy constructor with R-value reference parameter
Stack(Stack &&src)
    :msize(src.msize), mtop(src.mtop)
{
    cout << "Stack(Stack&&)" << endl;

    /*There is no need to re open the memory to copy data here. Give the src resources directly to the current object, and then empty the src*/
    mpstack = src.mpstack;  
    src.mpstack = nullptr;
}

// Overloaded function of assignment operator with R-value reference parameter
Stack& operator=(Stack &&src)
{
    cout << "operator=(Stack&&)" << endl;

    if(this == &src)
        return *this;
	    
    delete[]mpstack;

    msize = src.msize;
    mtop = src.mtop;

    /*There is no need to re open the memory to copy data here. Give the src resources directly to the current object, and then empty the src*/
    mpstack = src.mpstack;
    src.mpstack = nullptr;

    return *this;
}

The operation results are as follows:

Stack(int)             // Structure s
Stack(int)             // Construct tmp
Stack(Stack&&)         // Call the copy constructor with R-value reference to directly transfer tmp resources to temporary objects
~Stack()               // tmp deconstruction
operator=(Stack&&)     // Call the assignment operator with R-value reference to overload the function and directly give the temporary object resource to s
~Stack()               // Temporary object destructor
~Stack()               // s deconstruction

The program automatically calls the copy constructor with right value reference and the overload function of assignment operator, which greatly improves the efficiency of the program, because it does not reopen the memory to copy data.

mpstack = src.mpstack;  

The reason for direct assignment is that the temporary object will be destroyed soon, so there will be no shallow copy problem. We can directly assign the resources held by the temporary object to the new object.

Therefore, the temporary quantity will automatically match the member method of the right value reference version, in order to improve the efficiency of memory resource utilization.

The copy construction and assignment overloaded functions with R-value reference parameters are also called move constructors and move assignment functions. The move here refers to the movement of temporary resources to the current object, and the temporary object does not hold resources, which is nullptr. In fact, there is no data movement, no memory development and data copy.

Topics: C++ Algorithm