Rvalue references and move semantics

Purpose of new features

Rvalue reference (Rvalue Referene) is a new feature introduced in the new C++ standard (C++11, 11 represents 2011), which implements transfer semantics (Move Sementics) and precise transfer (Perfect Forwarding). Its main purpose is two-fold:

Eliminate unnecessary object copies when two objects interact, save computing and storage resources, and improve efficiency.
can define generic functions more concisely and clearly.

Definition of lvalue and rvalue

All expressions and variables in C++ (including C) are either lvalues ​​or rvalues. The popular definition of lvalue is a non-temporary object, an object that can be used in multiple statements. All variables meet this definition and can be used in multiple lines of code. They are all lvalues. Rvalues ​​are temporary objects that are only valid within the current statement. Please see the following examples:

  1. Simple assignment statement

    如:int i = 0;
    

    In this statement, i is an lvalue and 0 is a temporary value, which is an rvalue. In the following code, i can be referenced, but 0 cannot. Immediate numbers are all rvalues.

  2. An rvalue can also appear on the left side of an assignment expression, but it cannot be used as the object of the assignment, because the rvalue is only valid in the current statement, and the assignment is meaningless.

    如:((i>0) ? i : j) = 1;
    

    In this example, 0 appears as an rvalue to the left of "=". But the assignment object is i or j, both are lvalues.

    Before C++11, rvalues ​​could not be referenced. The maximum limit was to bind an rvalue with a constant reference, such as:

    const int &a = 1;
    

    In this case, the rvalue cannot be modified. But in fact rvalues ​​can be modified, such as:

    T().set().get();
    

    T is a class, set is a function that assigns a value to a variable in T, and get is used to retrieve the value of this variable. In this sentence, T() generates a temporary object, which is an rvalue. When set() modifies the value of the variable, it also modifies the rvalue.

    Since rvalues ​​can be modified, rvalue references can be implemented. Rvalue references can easily solve problems in actual engineering and achieve very attractive solutions.

Syntax notation for lvalues ​​and rvalues

The declaration symbol of an lvalue is "&", and to distinguish it from an lvalue, the declaration symbol of an rvalue is "&&".

Sample program:

void process_value(int& i) { 
 std::cout << "LValue processed: " << i << std::endl; 
} 
 
void process_value(int&& i) { 
 std::cout << "RValue processed: " << i << std::endl; 
} 
 
int main() { 
 int a = 0; 
 process_value(a); 
 process_value(1); 
}

operation result :

LValue processed: 0 
RValue processed: 1

The Process_value function is overloaded to accept lvalues ​​and rvalues ​​respectively. It can be seen from the output that temporary objects are processed as rvalues.

But if the temporary object is passed to another function through a function that accepts an rvalue, it will become an lvalue, because the temporary object becomes a named object during the transfer process.

Sample program:

void process_value(int& i) { 
 std::cout << "LValue processed: " << i << std::endl; 
} 
 
void process_value(int&& i) { 
 std::cout << "RValue processed: " << i << std::endl; 
} 
 
void forward_value(int&& i) { 
 process_value(i); 
} 
 
int main() { 
 int a = 0; 
 process_value(a); 
 process_value(1); 
 forward_value(2); 
}

operation result :

LValue processed: 0 
RValue processed: 1 
LValue processed: 2

Although the immediate number 2 is an rvalue when the function forward_value is received, it becomes an lvalue when process_value is received.

Definition of transfer semantics

Rvalue references are used to support transfer semantics. Transfer semantics can transfer resources (heap, system objects, etc.) from one object to another, which can reduce unnecessary creation, copying, and destruction of temporary objects, and can greatly improve the performance of C++ applications. Maintenance (creation and destruction) of temporary objects has a severe impact on performance.

Transfer semantics are opposite to copy semantics and can be compared to file cutting and copying. When we copy a file from one directory to another, the speed is much slower than cutting.

Through transfer semantics, resources in temporary objects can be transferred to other objects.

In the existing C++ mechanism, we can define copy constructors and assignment functions. To implement transfer semantics, you need to define a transfer constructor and you can also define a transfer assignment operator. For copying and assigning rvalues, the transfer constructor and transfer assignment operator are called. If the transfer constructor and transfer copy operator are not defined, then the existing mechanism is followed and the copy constructor and assignment operator will be called.

Ordinary functions and operators can also use rvalue reference operators to implement transfer semantics.

Implement the transfer constructor and transfer assignment function
Take a simple string class as an example to implement the copy constructor and copy assignment operator.

Sample program:

class MyString { 
private: 
 char* _data; 
 size_t   _len; 
 void _init_data(const char *s) { 
   _data = new char[_len+1]; 
   memcpy(_data, s, _len); 
   _data[_len] = '\0'; 
 } 
public: 
 MyString() { 
   _data = NULL; 
   _len = 0; 
 } 
 
 MyString(const char* p) { 
   _len = strlen (p); 
   _init_data(p); 
 } 
 
 MyString(const MyString& str) { 
   _len = str._len; 
   _init_data(str._data); 
   std::cout << "Copy Constructor is called! source: " << str._data << std::endl; 
 } 
 
 MyString& operator=(const MyString& str) { 
   if (this != &str) { 
     _len = str._len; 
     _init_data(str._data); 
   } 
   std::cout << "Copy Assignment is called! source: " << str._data << std::endl; 
   return *this; 
 } 
 
 virtual ~MyString() { 
   if (_data) free(_data); 
 } 
}; 
 
int main() { 
 MyString a; 
 a = MyString("Hello"); 
 std::vector<MyString> vec; 
 vec.push_back(MyString("World")); 
}

operation result :

Copy Assignment is called! source: Hello 
Copy Constructor is called! source: World

This string class basically meets the needs of our demonstration. In the main function, the operations of calling the copy constructor and the copy assignment operator are implemented. MyString("Hello") and MyString("World") are both temporary objects, that is, rvalues. Although they are temporary, the program still calls copy construction and copy assignment, resulting in meaningless resource application and release operations. If you can directly use the resources that have been applied for by the temporary object, you can not only save resources, but also save the time of resource application and release. This is the purpose of defining transfer semantics.

We first define the transfer constructor.

MyString(MyString&& str) { 
   std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
   _len = str._len; 
   _data = str._data; 
   str._len = 0; 
   str._data = NULL; 
}

Similar to the copy constructor, there are a few points to note:

  1. The symbol of the parameter (rvalue) must be an rvalue reference symbol, that is, "&&".

  2. Parameters (rvalues) cannot be constants because we need to modify the rvalue.

  3. The resource link and tag of the parameter (rvalue) must be modified. Otherwise, the rvalue's destructor releases the resource. Resources transferred to the new object will be invalid.

Now we define the transfer assignment operator.

MyString& operator=(MyString&& str) { 
   std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
   if (this != &str) { 
     _len = str._len; 
     _data = str._data; 
     str._len = 0; 
     str._data = NULL; 
   } 
   return *this; 
}

The issues that need to be noted here are the same as the transfer constructor.

After adding the transfer constructor and transfer copy operator, the running result of our program is:

Move Assignment is called! source: Hello 
Move Constructor is called! source: World

It can be seen from this that the compiler distinguishes between lvalues ​​and rvalues, and calls the transfer constructor and transfer assignment operator for rvalues. Save resources and improve the efficiency of program operation.

With rvalue reference and transfer semantics, when we design and implement classes, for classes that need to dynamically apply for a large number of resources, we should design transfer constructors and transfer assignment functions to improve the efficiency of the application.

Standard library function std::move

Since the compiler can only call the transfer constructor and transfer assignment function for rvalue references, and all named objects can only be lvalue references, if it is known that a named object is no longer used and you want to call the transfer constructor and transfer function on it Assignment function, that is, using an lvalue reference as an rvalue reference, how to do it? The standard library provides the function std::move, which converts an lvalue reference into an rvalue reference in a very simple way.

Sample program:

void ProcessValue(int& i) { 
 std::cout << "LValue processed: " << i << std::endl; 
} 
 
void ProcessValue(int&& i) { 
 std::cout << "RValue processed: " << i << std::endl; 
} 
 
int main() { 
 int a = 0; 
 ProcessValue(a); 
 ProcessValue(std::move(a)); 
}

operation result :

LValue processed: 0 
RValue processed: 0

std::move is very helpful in improving the performance of the swap function. Generally speaking, the general definition of the swap function is as follows:

   template <class T> swap(T& a, T& b) 
   { 
       T tmp(a);   // copy a to tmp 
       a = b;      // copy b to a 
       b = tmp;    // copy tmp to b 
}

With std::move, the definition of swap function becomes:

   template <class T> swap(T& a, T& b) 
   { 
       T tmp(std::move(a)); // move a to tmp 
       a = std::move(b);    // move b to a 
       b = std::move(tmp);  // move tmp to b 
}

Through std::move, a simple swap function avoids three unnecessary copy operations.

Perfect Forwarding

This article uses precise transmission to express this meaning. "Perfect Forwarding" has also been translated as perfect forwarding, precise forwarding, etc., which all mean the same thing.

Exact passing is suitable for scenarios where a set of parameters needs to be passed unchanged to another function.

"Unchanged" means not just that the value of the parameter remains unchanged. In C++, in addition to the parameter value, there are also two sets of attributes:

lvalue/rvalue and const/non-const. Exact delivery means that during the parameter transfer process, all these properties and parameter values ​​cannot be changed. In generic functions, such requirements are very common.

Here are some examples. The function forward_value is a generic function that passes an argument to another function, process_value.

forward_value is defined as:

template <typename T> void forward_value(const T& val) { 
 process_value(val); 
} 
template <typename T> void forward_value(T& val) { 
 process_value(val); 
}

The function forward_value must overload two types for each parameter, T& and const T&, otherwise, the following four different types of parameters cannot be satisfied at the same time in the call:

int a = 0; 
 const int &b = 1; 
 forward_value(a); // int& 
 forward_value(b); // const int& 
forward_value(2); // int&

A parameter must be overloaded twice, that is, the number of function overloads is directly proportional to the number of parameters. The number of definitions of this function is very inefficient for programmers. Let's see how rvalue references can help us solve this problem:

template <typename T> void forward_value(T&& val) { 
 process_value(val); 
}

It only needs to be defined once and accepts an rvalue reference parameter, and all parameter types can be passed to the target function intact. Four types of calls without type parameters can be satisfied. The left and right value attributes and const/non-cosnt attributes of the parameters are completely passed to the target function process_value. Isn't this solution simple and elegant?

int a = 0; 
const int &b = 1; 
forward_value(a); // int& 
forward_value(b); // const int& 
forward_value(2); // int&&

The derivation rules for T&& defined in C++11 are:

An rvalue argument is an rvalue reference, and an lvalue argument is still an lvalue reference.

In a word, the properties of the parameters remain unchanged. This also perfectly realizes the complete transfer of parameters.

Rvalue reference, on the surface, just adds a reference symbol, but it has a great impact on C++ software design and class library design. It can not only simplify the code, but also improve the efficiency of the program. Every C++ software designer and programmer should understand and be able to apply it. If we have dynamically applied resources when designing a class, we should also design transfer constructors and transfer copy functions. When designing a class library, you should also consider the usage scenarios of std::move and use it actively.

Summarize

Rvalue references and transfer semantics are an important feature in the new C++ standard. Every professional C++ developer should master and apply it to actual projects. When you have the opportunity to refactor your code, you should also think about whether you can apply the new code. Before using it, you need to check the compiler support.

おすすめ

転載: blog.csdn.net/rankun1/article/details/102860267