New Feature of C ++ 0x in VC10: Right Value reference (rvalue references) (1)

Source: Internet
Author: User

The original text address is here. For more information, see the source.

The first part of this series describesLambda expressions,Auto keywordAndStatic_assert.

This article describesRight reference, And two new concepts that come with it:Move Semantics)AndPerfect Forwarding (perfect forwarding). This article will take a long time, because I will explain in detail how the right value reference works. At the beginning, you may feel a bit messy, because few C ++ 98/03 programmers are familiar with it.Left (lvalue)AndRight Value (Rvalue)Yes.

However, you do not need to back up because it is very easy to use the right value reference. In your own code, whether it is implementing the move semantics or perfect forwarding, it can all be attributed to the simple mode that I will describe below. In addition, learning the right value reference is very worthwhile, because the move semantics can improve performance by an order of magnitude, and perfect forwarding makes it very easy to write highly common code.

The left and right values in C ++ 98/03

To understand the reference of the right value in C ++ 0x, you must first understand the left and right values in C ++ 98/03. According to my understanding, the left value is the value assigned to the left. the right value cannot be placed on the left but only on the right ...... I don't know, right)

The terms "Left value" and "Right Value" are messy, because their history is messy ...... These concepts were originally derived from C, and then made complicated in C ++. In order to save time, I skipped their history and explained how they work in C ++ 98/03.

The left value exists continuously beyond a single expression. For example, OBJ, * PTR, PTR [Index], ++ X, all belong to the left value.

The right value is a temporary object, which only exists in a single expression and is destroyed after the expression ends. For example, 1729, X + Y, STD: string ("meow"), and X ++ are both right values.

(: Lvalue-> left value, const lvalue-> constant left value, rvalue-> right value, const rvalue-> constant right value, Chinese or English as needed)

Note the differences between X ++ and ++ X: If we declare int x = 0, x is the left value, and the statement declares a persistent object. ++ X is also a left value. It modifies and returns the Persistent Object X. However, X ++ is the right value because it copies the Persistent Object X, after the value of X is modified, the original copy is returned, which is temporary. Both x ++ and ++ x increase by X, but ++ x returns the auto-increment x itself. x ++ returns a copy of X before auto-increment, this copy is temporary. This is why ++ X is the left value while X ++ is the right value. To determine the left and right values, you don't have to worry about what this statement does. Instead, you only need to check what it is named (whether it is a persistent object or a temporary object ).

If you want to establish an intuition to determine the left and right values, another quick way is to see if "Is it valid to retrieve its address ?" If the address can be obtained, it is the left value; otherwise, it is the right value. For example: & OBJ, & * PTR, & PTR [Index], and & ++ X are both acceptable, but & 1729, & (x + y), & STD :: string ("meow") and & X ++ are invalid. Why can this problem be determined? The bitwise operator requires that the operation object be a left value (C ++ 03 5.3 1/2 ). Why is there such a requirement? It is correct to get the address of a persistent object, but it is dangerous to get the address of a temporary object because the temporary object will be destroyed soon.

The preceding example ignores Operator overloading. Calling an overloaded operator is also a function call. "Function call is a left value only when the returned value is a reference ." (C ++ 03 5.2.2/10 ). Therefore, given vector V (10,172 9);, V [0] is a left value, because operator [] () returns an int & (and & V [0] is valid ). Given string S ("foo"); and string T ("bar");, S + T is the right value because operator + () returns the string type (and & (S + T) is invalid ).

The left and right values can both be variables or constants, for example:

String one ("cute ");
Const string two ("fluffy ");
String three () {return "kittens ";}
Const string four () {return "are an essential part of a healthy diet ";}
One; // modifiable lvalue
Two; // const lvalue
Three (); // modifiable rvalue
Four (); // const rvalue

Type & bind with lvalue (and can be used to observe and modify them ). It cannot be bound to the const lvalue because it violates the constant constraint. It cannot be bound to rvalues, because it may be extremely dangerous. Accidental modification of the temporary object will only result in the disappearance of the temporary object along with the modification operation, which will lead to some hard-to-find and annoying bugs. Therefore, C ++ prohibits such modification. (I should mention that VC has an evil extension that allows you to do this, but if you compile it using the/W4 option, the compiler will trigger an alarm .) Also, it cannot be bound to the const rvalue, because this may lead to double errors (: 1. Constant referenced by the extraordinary reference type; 2. reference by the Left value to the right value) (careful readers may have noticed that I have not mentioned the derivation of template parameter types here)

Const type & can be bound to anything: lvalue, Rvalue, const lvalue, const rvalue (and can be used to observe their changes ).

A reference type is named. Therefore, a reference bound to the right value itself is the left value. (Because only constant reference can be bound to the right value, it will be a const lvalue) This is a bit of a limit port, and it will lead to some big problems, so I will explain it further. Given a function signature void observe (const string & Str), even if the right value like three () above is passed as a parameter when calling observe, in this function, STR is also a const lvalue, and its address can be obtained and available before the function returns. You can also call observe ("Purr"), which causes a temporary String object to be constructed and binds STR to it. The return values of three () and four () are also the right values, but within the observe function, STR is a name, so it is the left value. As I mentioned above, "the Left or Right value is for expressions rather than objects ." Of course, because STR can be bound to a temporary object that will be destroyed in the future, it is wrong to save its address and use it after observe () returns.

Have you ever bound a right value to a const type & above and obtained its address? Yes, you must have done that! When you overload a value assignment operator Foo & operator = (const Foo & Other), there will be Detection If (this! = & Other) {copy stuff;} return * This; in this way, when you assign values from a temporary object, the above situation occurs, such as Foo make_foo (); Foo F; F = make_foo ();.

At this time, some students asked, "I cannot bind type & to rvalue, and I cannot assign anything to be associated with rvalue. Can I modify the right value? What is the difference between const rvalue and rvalue ?" This is a very good question! In C ++ 98/03, const rvalue and rvalue are slightly different: they can be called on the rvalue object, but not on the object of the const rvalue attribute. In C ++ 0x, the answer changes dramatically, leading to the appearance of the move semantics.

Congratulations! Now you have the "left/right senses": You can see an expression to identify whether it is the left or right value. In combination with the const attribute, You can accurately analyze the definition of void mutate (string & ref) and the above variables. mutate (one) is OK, mutate (two ), mutate (three (), mutate (four (), mutate ("Purr") are invalid. All observe (one), observe (three (), observe (four (), and observe ("Purr") are valid. If you are a C ++ 98/03 programmer, you used to "Instinct" to tell you which calls are illegal and which ones are valid. But now you have the "left/right senses", which can precisely tell you why mutate (three () does not pass compilation (because three () is a right value, type & cannot be bound to the right value ). Is this useful? It is useful for people who study grammar, but it is useless for common programmers. After all, you have achieved this capability without understanding too much details about the left and right values. But note that compared with C ++ 98/03, C ++ 0x requires more powerful left and right value judgment capabilities. You now have this capability, so let's continue!

Object replication Problems

C ++ 98/03 combines powerful abstraction capabilities with powerful execution efficiency, but it has a problem: it relies heavily on Object replication. Objects all have value semantics, So copying an object does not affect the source object, and its copy is also independent. Value semantics is great, but it sometimes leads to unnecessary copying of large objects such as string and vector. In some cases, return value optimization (RVO) and name return value optimization named return value optimization (nrvo) can alleviate this problem, but they do not completely eliminate all unnecessary replication behaviors.

The most unnecessary copying behavior is to copy the objects to be destroyed. Will you immediately drop the original one after copying the form overnight? This is a waste. You should keep the original one instead of copying one and then throwing away the original one. Here is an example of what I call "killer", from the Standards Board (n1377 ). Suppose you have a string:

String S0 ("My mother told me that ");
String S1 ("cute ");
String S2 ("fluffy ");
String S3 ("kittens ");
String S4 ("are an essential part of a healthy diet ");

Then you connect them:

String DEST = S0 + "" + S1 + "" + S2 + "" + S3 + "" + S4;

How efficient is this sentence? (We are not worried about the efficiency of this special case. It only runs for several microseconds. We are worried about the efficiency of the entire language)

Each call to operator + () returns a temporary String object. The above calls operator + () for a total of 8 times, so there are 8 temporary strings. Each time a temporary object is constructed, its constructor dynamically allocates some memory and copies all the previously connected characters. Then, the Destructor is called and the memory is released.

In fact, each string connection copies all the characters of the previously connected string. Therefore, as the number of connections increases, the complexity increases to a square level. Ah! This is a waste! How can we avoid this situation?

The problem lies in operator + (). It accepts two const strings & or one const string & and one const char * as parameters, it cannot determine whether the accepted parameter is the left or right value. Therefore, it creates and returns a temporary string each time. Why is it important to know the Left or Right value?

When we look at S0 + ", it is absolutely necessary to create a temporary object. S0 is the left value. It is named as a persistent object, so we cannot modify it. But when we look at (S0 + "") + S1, we should directly add the content of S1 to the temporary object created by (S0 +, instead of creating a new temporary object, throwing away the first temporary object. This isThe key to moving Semantics: Because (S0 + "") is a right value, an expression representing a temporary object, there is no other place in the program to observe this variable. If we can detect that this is a right value, we can modify it without affecting any other places. Operator + () does not want to modify its parameters, but if the parameter is the right value that can be modified, can it be modified? Using this method, each call to operator + () expands the character on the temporary string created for the first time, which completely eliminates unnecessary memory management and replication, bringing us linear complexity, oh yeah!

Technically speaking, in C ++ 0x, each call to operator + () still returns an independent temporary variable. However, in (S0 + "") + S1, the second + returned Temporary Variable steals the memory of the first + created temporary variable, and connect the content of S1 to the stolen memory (which may result in a larger memory request ). "Stealing" also includes pointer modification: The second temporary variable copies the memory of the first temporary variable and sets its memory pointer to null. When the first temporary variable releases the memory, the pointer is null, so the Destructor does not need to do anything.

Generally, the ability to check the modifiable right value makes you a "resource thief ". If the referenced right value contains any resources (such as memory), you can steal its resources without having to copy it, because it will be destroyed immediately. By stealing the resources above the right value to construct an object or assigning a value, it is usually called "move". objects that can be moved have the "Move Semantics ".

This is useful in many places, such as the redistribution of vectors. When a vector needs a larger capacity, it needs to move the data object from the old memory block to the new memory block after the memory is re-allocated, calling their copy constructor may be expensive (for a vector <string>, each copy of the string needs to be allocated memory to copy the entire string ). But wait! Objects in the old memory block will be destroyed immediately, so we can move them instead of copying them. In this case, the elements in the old memory block are persistent storage, and the expression that references this element like old_ptr [Index] is the left value. If we think of them as the right value, we can move them to eliminate the call to copy constructors. (Saying "I want to regard this left value as the right value" is equivalent to saying "I know this is a left value, representing a persistent object, but I don't care, because I am about to destroy it, or copy it again, and so on. So if you can steal its resources, do that !")

The right value reference of C ++ 0x gives us the ability to detect the right value and steal the right value resources, which makes the move semantics a reality. The right value reference also allows us to regard the left value as the right value and implement the move semantics. Now, let's see how the right-value semantics works.

Right Value reference: initialization

C ++ 0x introduces a new reference type, and the right value references: Type & and const type &&. In the current C ++ 0x working draft, n2798 8.3.2/2 said: "The reference through & declaration is called the left value reference, and the reference through & declaration is called the right value reference. The left reference and right reference are different types. Unless explicitly stated, they are semantically equivalent and often referred to as references ." This means you need to learn their differences.

Compared with the left value reference, the right value reference has different behaviors during initialization and reload. The difference is what type of objects they tend to bind (initialization) and what type of objects tend to be preferentially bound to them (reload ). Let's first look at initialization:

  • We already know that type & tends to bind lvalue. None of them works (const lvalue, Rvalue, const Rvalue ).
  • We already know that const type & tends to bind all types.
  • Type & tends to bind lvalue and rvalue, but cannot bind const lvalue and const rvalue (this violates the constant constraint ).
  • Const type & tends to bind all types.

These rules look mysterious, but they are derived from the following two:

  • Constant constraints are observed. variable references cannot be bound to constants.
  • Avoid modifying temporary variables and prevent the left value reference from being bound to the right value.

If you prefer to see the error message provided by the compiler, the following is an example:

C:/temp> type initialization. cpp
# Include <string>
Using namespace STD;

String modifiable_rvalue (){
Return "cute ";
}

Const string const_rvalue (){
Return "fluffy ";
}

Int main (){
String modifiable_lvalue ("kittens ");
Const string const_lvalue ("Hungry hungry zombies ");

String & A = modifiable_lvalue; // line 16
String & B = const_lvalue; // line 17-Error
String & C = modifiable_rvalue (); // line 18-Error
String & D = const_rvalue (); // line 19-Error

Const string & E = modifiable_lvalue; // line 21
Const string & F = const_lvalue; // line 22
Const string & G = modifiable_rvalue (); // line 23
Const string & H = const_rvalue (); // line 24

String & I = modifiable_lvalue; // line 26
String & J = const_lvalue; // line 27-Error
String & K = modifiable_rvalue (); // line 28
String & l = const_rvalue (); // line 29-Error

Const string & M = modifiable_lvalue; // line 31
Const string & n = const_lvalue; // line 32
Const string & O = modifiable_rvalue (); // Line 33
Const string & P = const_rvalue (); // line 34
}

C:/temp> Cl/ehs/ nologo/W4/wx initialization. cpp
Initialization. cpp
Initialization. cpp (17): Error c2440: 'initializing': cannot convert from 'const STD: string' to 'std: string &'
Conversion loses qualifiers
Initialization. cpp (18): Warning c4239: Nonstandard extension used: 'initializing': conversion from 'std: string' to 'std: string &'
A non-const reference may only be bound to an lvalue
Initialization. cpp (19): Error c2440: 'initializing': cannot convert from 'const STD: string' to 'std: string &'
Conversion loses qualifiers
Initialization. cpp (27): Error c2440: 'initializing': cannot convert from 'const STD: string' to 'std: string &&'
Conversion loses qualifiers
Initialization. cpp (29): Error c2440: 'initializing': cannot convert from 'const STD: string' to 'std: string &&'
Conversion loses qualifiers

Binding the right value to the right value reference can be used to modify temporary variables.

Even if the left-and right-value references behave similarly during initialization (only 18 rows and 28 rows are different), they behave quite differently in overloading.

Right Value reference: overload Determination

You are already familiar with function overloading when parameters are variables and constant left value references. In C ++ 0x, a function can reference overload with a constant or a constant's right value. Given all four reloads of A mona1 function, you should have discovered that each expression tends to be bound to the reference corresponding to it:

C:/temp> type four_overloads.cpp
# Include <iostream>
# Include <ostream>
# Include <string>
Using namespace STD;

VoidMeow (string & S){
Cout <"meow (string &):" <S <Endl;
}

VoidMeow (const string & S){
Cout <"meow (const string &):" <S <Endl;
}

VoidMeow (string & S){
Cout <"meow (string &):" <S <Endl;
}

VoidMeow (const string & S){
Cout <"meow (const string &):" <S <Endl;
}

String strange (){
Return "Strange ()";
}

Const string charm (){
Return "charm ()";
}

Int main (){
String up ("up ");
Const string down ("down ");

Meow (up );
Meow (down );
Meow (strange ());
Meow (charm ());
}

C:/temp> Cl/ehs/ nologo/W4 four_overloads.cpp
Four_overloads.cpp

C:/temp> four_overloads
Meow (string &): Up
Meow (const string &): Down
Meow (string &): Strange ()
Meow (const string &): charm ()

In practice, heavy loading of Type &, const type &, type &, const type & is not very practical. A more interesting overload is the set const type & and type &&:

C:/temp> type two_overloads.cpp
# Include <iostream>
# Include <ostream>
# Include <string>
Using namespace STD;

VoidPurr (const string & S){
Cout <"Purr (const string &):" <S <Endl;
}

VoidPurr (string & S){
Cout <"Purr (string &):" <S <Endl;
}

String strange (){
Return "Strange ()";
}

Const string charm (){
Return "charm ()";
}

Int main (){
String up ("up ");
Const string down ("down ");

Purr (up );
Purr (down );
Purr (strange ());
Purr (charm ());
}

C:/temp> Cl/ehs/ nologo/W4 two_overloads.cpp
Two_overloads.cpp

C:/temp> two_overloads
Purr (const string &): Up
Purr (const string &): Down
Purr (string &): Strange ()
Purr (const string &): charm ()

Why is this? The binding rules are as follows:

  • The above initialization rules have veto power.
  • The left value is very strong and tends to be bound to the left value reference. the right value is very strong and tends to be bound to the right value reference.
  • Variable expressions tend to be bound to uncommon references, but tend to be slightly weaker.

(For "deny", I mean when determining the matching candidate function, the function that is determined to be unfeasible will be immediately excluded) Let's judge based on the rules:

  • For purr (up), purr (const string &) and purr (string &) are not blocked by initialization rules. Up is a left value, so it strongly wants to bind it to the left value reference purr (const string. Up is variable, so it tends to be bound to the variable reference purr (string. A strong tendency for purr (const string &) to win.
  • For purr (down), the initialization rule denies purr (string &) based on constant constraints, so purr (const string &) wins.
  • For purr (strange (), purr (const string &) and purr (string &) are not blocked by initialization rules. Strange () is a right value, so it strongly tends to be bound to the right value reference purr (string. Strange () is variable, so it is relatively weak and tends to be bound to a very large reference purr (string. We strongly prefer purr (string &) to win.
  • For purr (charm (), the initialization rule rejects purr (string &) based on constant constraints, so purr (const string &) wins.

It is worth noting that when you reload const type & and type &, the right value of the variable is bound to type &, and other values are bound to const type. Therefore, this set of overloading is very suitable for moving semantics.

Important: When a function returns by value (instead of returning a reference), it should return type (like strange () instead of const type (like charm ). Because the latter has almost no effect (except to prohibit calls to function of a very large number of Members), it also hinders the optimization of the moving semantics.

Move semantics: Model

Here is a simple class, remote_integer, which stores a pointer pointing to a dynamically allocated Int. You should be familiar with its default constructor, unary constructor, copy constructor, overload assignment operator, and destructor. I added the move constructor and the move overload assignment operator to it. They were compiled by the # ifdef movable condition, I used it to demonstrate what happened when there were both functions and none of them, and the real code would not do that.

C:/temp> type remote. cpp
# Include <stddef. h>
# Include <iostream>
# Include <ostream>
Using namespace STD;

Class remote_integer {
Public:
Remote_integer (){
Cout <"default constructor." <Endl;

M_p = NULL;
}

Explicit remote_integer (const int N ){
Cout <"unary constructor." <Endl;

M_p = new int (N );
}

Remote_integer (const remote_integer & Other ){
Cout <"copy constructor." <Endl;

If (other. m_p ){
M_p = new int (* Other. m_p );
} Else {
M_p = NULL;
}
}

# Ifdef movable
Remote_integer (remote_integer & Other ){
Cout <"Move constructor." <Endl;

M_p = Other. m_p;
Other. m_p = NULL;
}
# Endif // # ifdef movable

Remote_integer & operator = (const remote_integer & Other ){
Cout <"Copy assignment operator." <Endl;

If (this! = & Other ){
Delete m_p;

If (other. m_p ){
M_p = new int (* Other. m_p );
} Else {
M_p = NULL;
}
}

Return * this;
}

# Ifdef movable
Remote_integer & operator = (remote_integer & Other ){
Cout <"Move assignment operator." <Endl;

If (this! = & Other ){
Delete m_p;

M_p = Other. m_p;
Other. m_p = NULL;
}

Return * this;
}
# Endif // # ifdef movable

~ Remote_integer (){
Cout <"destructor." <Endl;

Delete m_p;
}

Int get () const {
Return m_p? * M_p: 0;
}

PRIVATE:
Int * m_p;
};

Remote_integer square (const remote_integer & R ){
Const int I = R. Get ();

Return remote_integer (I * I );
}

Int main (){
Remote_integer A (8 );

Cout <A. Get () <Endl;

Remote_integer B (10 );

Cout <B. Get () <Endl;

B = square ();

Cout <B. Get () <Endl;
}

C:/temp> Cl/ehs/ nologo/W4 remote. cpp
Remote. cpp

C:/temp> remote
Unary constructor.
8
Unary constructor.
10
Unary constructor.
Copy assignment operator.
Destructor.
64
Destructor.
Destructor.

C:/temp> Cl/ehs/ nologo/W4/DmovableRemote. cpp
Remote. cpp

C:/temp> remote
Unary constructor.
8
Unary constructor.
10
Unary constructor.
Move assignment operator.
Destructor.
64
Destructor.
Destructor.

Note the following points:

  • The copy and move constructors are overloaded, and the copy and move assignment operators are also overloaded. We have seen the occurrence of const type & and type & function overloading. This is why B = square (a) automatically selects the move assignment operator when it is available.
  • The move copy constructor and the move assignment operator steal memory from other places rather than dynamically applying for memory. When stealing others' memory, we directly copy the memory pointer and set it to null. When the object is released, the Destructor does not release the memory repeatedly.
  • The move copy constructor and the move value assignment operator both require self-assignment detection. Because a type such as int can do X = x without harm, user-defined types should also support auto-assignment. Self-assignment usually does not occur in handwritten program code, but often occurs in some algorithms, such as STD: Sort (). In C ++ 0x, algorithms like STD: Sort () can move objects instead of copying them. At this time, potential auto-assigned values exist.

At this time, you will ask whether the moving semantics affects automatically generated ("implicitly declared") constructors and value assignment operators?

  • CompilerNeverAutomatically generate the move constructor and the move assignment operator.
  • Including copy constructorsAndMove constructor. Any user-defined constructor will prevent the compiler from generating default constructor.
  • The user-defined copy constructor prevents the generation of implicit copy constructor, but the User-Defined move copy constructorNoBlock the generation of implicit copy constructors.
  • Similarly, the User-Defined move assignment operatorNoPrevents the generation of implicit value assignment operators.

Basically, automatic generation rules are not affected by the move semantics. Except for the declaration of the move constructor, declaring any constructor will prevent the compiler from generating default constructor.

Before completion, go down [translation] new feature of C ++ 0x in VC10: Right Value reference (rvalue references) (2)

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.