C++11: rvalue references and universal references

introduction

Both rvalue references and universal references are new concepts introduced by C++11; since they are related to &&, they are put into a topic for analysis and discussion here.

In general, with the assignment operator = as the boundary, the left side of = is called lvalue, and the right side of = is called rvalue. For example: int value = 3; value is lvalue, 3 is rvalue. But this is just a formality, not a complete generalization of lvalues ​​and rvalues, because in some cases, this rule does not apply. For example: int aValue = value; value appears on the right side of =, but value is not an rvalue.

In addition to rvalue and lvalue, there are multiple attribute classifications in modern C++. They are xvalue, prvalue and glvalue. The relationship between them is shown in Figure 1.

ca07a3ac412d4729b597f5e02630d431.png
Figure 1 Attribute Classification Diagram

As shown in Figure 1, glvalue = lvalue lvaue + xvalue xvalue; rvalue = prvalue prvalue + xvalue xvalue. The change is that the lvalue before C++11 becomes the prvalue in the C++11 standard; the lvalue of C++11 is basically the same as the previous standard.

Regarding lvalue, the C89 standard has this discussion: the definition of lvalue as an object locator, that is to say, an object that can take an address and will not die immediately is an lvalue, so l in lvalue can be understood as location.

The prvalue prvalue is the rvalue of the C89 standard, which means a read-only object without a clear address, and the r in the prvalue can be understood as read. A prvalue is either a literal constant; or evaluates to a literal or anonymous temporary variable. Temporary variables returned by non-references, temporary variables generated by expressions, primitive literals, and lambda expressions are all prvalues.

The xvalue is the object that C++11 introduces that is related to the rvalue reference and will be destroyed, and the value that can be moved. The two most obvious differences between lvalues ​​and rvalues ​​are: lvalues ​​have persistent state, while rvalues ​​are either prvalues ​​(literal values, temporary objects created during expression evaluation) or are removable temporary object.

References are introduced by the C++98 standard, because references can only bind lvalues; so Morden C++ (C++11 and later standards) introduces rvalue references to solve the problem of binding rvalues. Rvalue references can be said to be a traditional Expansion of references. After being expanded by Morden C++, references in traditional C++ become lvalue references in Morden C++; Morden C++ introduces rvalue references, thereby leading to move semantics;

lvalue reference

The C++98 standard introduces lvalue references, which are defined by &, and its binding rules can be summarized as follows:

  • an lvalue binding binds to an lvalue reference;
  • cannot bind an rvalue to an lvalue reference, unless it is an lvalue reference of type const

Specific examples can be referred to:

int b = 5;
int& a = b;      // 可以正常编译
int& a1 = 500;   // 无法通过编译
const int& a2 = 500; // 可以通过编译

rvalue reference

The so-called rvalue reference is a reference that must be bound to an rvalue. We generally use && instead of & to obtain rvalue references. So it can be seen that the rvalue reference can be bound to the object to be destroyed (the xvalue), and the resource that binds the xvalue to the rvalue reference is equivalent to the rvalue reference is moved to an object, which can extend the xvalue life cycle. For example:

struct Circle
{
    Circle(const double& circle) : circle(circle) {}
    double circle;
};

Circle&& newCircle()
{
    return Circle(1.0);
}

In addition to xvalues, rvalue references can also bind prvalues ​​(that is, literal constants), for example:

double&& radius = 5.30;

Rvalue references can bind xvalues ​​or prvalues, but cannot bind lvalues. For example:

int b = 5;
int&& a = b;      // 无法编译

Constant rvalue references can refer to prvalues ​​and dying values, but can bind lvalues. For example:

int b = 5;
const int&& a = 100;
const int&& width = newCircle().circle;
const int&& c = b; // 无法编译 

universal reference

The English of universal reference is universal reference. In addition to "universal reference", there is another translation "undefined reference". The general definition method of universal reference is T&&. Note that T here is a deducible template type, not a type in the ordinary sense. Universal references generally exist in the following two scenarios:

  • A template function whose argument type is T&& must have type deduction
  • The reference declaration of auto && must also have type deduction
void f(int &&value)     // “&&” means rvalue reference

int && var = someValue; // “&&” means rvalue reference

auto&& var2 = var1;     //  “&&” means universal reference

template<typename T>
void f(std::vector<T>&& param);  // “&&” means rvalue reference
 
template<typename T>
void f(T&& param);               // “&&” mean universal reference

For universal references, there are two points to note:

  • The declaration form must be T && or auto &&
  • Only when the deduction occurs, && is called a universal reference, otherwise it is counted as an rvalue reference

In order to better illustrate the above two precautions, let us analyze with an example:

Example 1: Combining T with a container

template<typename T>
void fun(std::vector<T>&& arg)   // “&&” means rvalue reference

Here, && is an rvalue reference, because the declaration type of the input parameter is std::vector<T>&&, not T&&

Example 2: const T&&

template<typename T>
void fun(const T&& arg);               // “&&” means rvalue reference

Due to the existence of const modified T&&, && here is also an rvalue reference, not a universal reference.

Example 3: Member function T&&

template <class T, class Allocator = allocator<T>>
class vector 
{
public:
    ...
    void push_back(T&& x);       // fully specified parameter type ⇒ no type deduction;
    ...                          // &&  rvalue reference
};

Although it is declared as T&& here, the type of T is already determined when the template is instantiated, so T does not need to be compiled for type deduction. So && in the push_back statement is an rvalue reference rather than a universal reference.

Example 4: variable length parameter template Args&&... args

template <class T, class Allocator = allocator<T> >
class vector 
{
public:
    ...
    template <class... Args>
    void emplace_back(Args&&... args); // deduced parameter types ⇒ type deduction;
    ...                                // && ≡ universal references
};

In this example, although the type of T can be determined when the template is instantiated, the type of each args still needs to be deduced one by one. So here && is a universal reference rather than an rvalue reference.

reference collapsing

There is a difference between lvalue and rvalue in template deduction. For an lvalue of type T, the template will be deduced as T& type; but for an rvalue of type T, the template will be deduced as T.

template<typename T>
void func(T&& arg);
 
...
 
int x;
 
...
 
func(10);                           // invoke func on rvalue
func(x);                            // invoke func on lvalue

For the function call func(10), T in func<T> will be deduced as int, so the form of func is similar to

void func(int&& arg);             // 右值变量func的实例化

But for func(x), the T in func<T> will be deduced as int&, so the form of func is like

void func(int& && arg);           // 左值变量func的实例化

But func(int& && arg) is an illegal function declaration. To solve this problem C++11 introduced "reference folding".

Since there are lvalue references and rvalues, there are 4 forms of reference combinations, they are: lvalue reference to lvalue reference, lvalue reference to rvalue reference, rvalue reference to lvalue reference, and rvalue reference to rvalue reference. These four forms of reference collapsing rules:

  • rvalue reference to rvalue reference becomes rvalue reference after folding
  • For the other three combinations, all references become rvalue references after being folded.

Table 1: Reference folding rules
Template entry declaration Input type type of reference fold
lvalue reference lvalue reference lvalue reference
lvalue reference rvalue reference lvalue reference
rvalue reference lvalue reference lvalue reference
rvalue reference rvalue reference rvalue reference

Like lvalue references and rvalue references, universal references also need to be initialized. Based on the reference folding rules, the initialization rules of universal references can be summarized as follows:

  • If the universal reference is initialized by an lvalue expression, then the universal reference will become an lvalue reference. The lvalue initializer can be an addressable expression, and the lvalue reference expression is such as T&, const T&.
  • If a universal reference is initialized by an rvalue, then the universal reference becomes an rvalue reference, and the initial expression of the rvalue can be either a prvalue or a dummy value.
int&& var1 = 100;   // var1 is a rvalue reference, but var1 is a lvalue
auto&& var2 = var1; // var2 is an lvalue reference, since we can take the address of var1

Summarize

In summary, assuming that T is a concrete type, then the reference can be summarized as follows:

  • Lvalue references, using T&, can only bind lvalues;
  • Rvalue references, using T&&, can only bind rvalues;
  • Constant lvalue reference, using const T&, can bind either lvalue or rvalue;
  • A named rvalue reference is considered an lvalue by the compiler;
  • Constant rvalue references can refer to constant rvalues ​​and non-const rvalues.

Also corresponding to universal references and introduction of folding, the C++11 folding rules are simpler:

  • As long as there is an lvalue reference during reference folding, the folded type is an lvalue reference, otherwise it is an rvalue reference.

Guess you like

Origin blog.csdn.net/liuguang841118/article/details/129339712