Return argument passed by rvalue reference

If I have class A and functions

 A f(A &&a) { doSomething(a); return a; } A g(A a) { doSomething(a); return a; } 

the copy constructor is called when A returns from f , but the move constructor is used when returning from g . However, from what I understand, f can only be passed an object that can be safely moved (temporary or an object marked as movable, for example, using std::move ). Is there any example where it would be safe to use the move constructor when returning from f ? Why do we need A have an automatic storage duration?

I read the answers here , but the top answer shows that the specification should not allow movement when passing A other functions in the body of the function; he does not explain why moving when returning is safe for g , but not for f . Once we get back to the return statement, we no longer need A inside f .

Update 0

So, I understand that the time frame is available until the end of the full expression. However, the behavior when returning from f is still contrary to semantics rooted in the language, which is safe to move temporary or xvalue. For example, if you call g(A()) , the temporary one moves into the argument for g , although there may be links to the temporary storage somewhere. The same thing happens if we call g value of x. Since only temporary and xvalues ​​are tied to rvalue links, it seems that they are consistent with semantics, we should still move A when returning from f , since we know that A was passed either a temporary or x value.

+6
source share
3 answers

Second attempt. Hope this is more concise and clear.

I am going to ignore RVO almost completely for this discussion. This makes it really confusing as to what should happen without optimization — it's just moving around copy semantics.

To help with this, the link here will be very useful in value types in C ++ 11 .

When to move?

naming

They never move. They refer to variables or storage locations that are potentially referenced elsewhere, and therefore should not contain their contents in another instance.

prvalue

The above define them as "expressions that do not have identity." Clearly, nothing else can refer to an unnamed value so that they can be moved.

Rvalue

The general case of the "right" meaning, and the only thing from which they can be transferred. They may or may not have named links, but if they do, this is the last such use.

xvalue

This is a kind of mixture of both - they have an identity (are a link), and they can be moved. They should not have a named variable. Cause? They are eXpiring values ​​that must be destroyed. Consider their "final link". xvalues ​​can only be generated from rvalues, so / how std::move works when converting lvalues ​​to xvalues ​​(via the result of a function call).

glvalue

Another type of mutant with its cousin rvalue, it can be either the value of x or lvalue - it has an identity, but it is unclear whether this is the last reference to the variable / storage or not, so it is unclear whether or not it can move from.

Resolution Procedure

If there is an overload that can either accept const lvalue ref or rvalue ref and pass rvalue, the value of rvalue is limited, otherwise the version of lvalue is used. (moving for rvalues, copying otherwise).

Where can this happen

(suppose all types A are not mentioned)

This only happens when the object is “initialized from a value x of the same type”. xvalues ​​are bound to rvalues, but not as limited as pure expressions. In other words, movable things are more than unnamed links, they can also be the "last" link to an object with respect to compiler awareness.

initialization

 A a = std::move(b); // assign-move A a( std::move(b) ); // construct-move 

passing an argument to a function

 void f( A a ); f( std::move(b) ); 

return function

 A f() { // A a exists, will discuss shortly return a; } 

Why does this not happen in f

Consider this variation on f:

 void action1(A & a) { // alter a somehow } void action2(A & a) { // alter a somehow } A f(A && a) { action1( a ); action2( a ); return a; } 

You cannot treat A as an lvalue inside f . Since this is an lvalue , it must be a reference, explicit or not. Each simple variable is technically a reference to itself.

Where we travel. Since A is an lvalue for f purposes, we actually return an lvalue.

To explicitly generate an rvalue, we must use std::move (or generate the result of A&& another way).

Why does this happen in g

With this under our belts consider g

 A g(A a) { action1( a ); // as above action2( a ); // as above return a; } 

Yes, A is an lvalue for the purposes of action1 and action2 . However, since all references to A exist only inside g (is it a copy or a moved copy), it can be considered xvalue in the return.

But why not in f ?

There is no special magic for && . Indeed, you should think of it as a link in the first place. The fact that we require an rvalue reference in f , unlike an lvalue reference with A& , does not alter the fact that, as a link, it must be an lvalue, because the storage location of A is external to f , and as for any compiler.

The same does not apply in g , where it is clear that the storage A is temporary and exists only when g called and at no other time. In this case, this is explicitly the value of x and can be moved.


rvalue ref vs lvalue ref and reference pass security

Suppose we overload a function to accept both types of links. What will happen?

 void v( A & lref ); void v( A && rref ); 

The only time void v( A&& ) will be used as described above ("Where it can happen"), otherwise void v( A& ) . That is, rvalue ref will always try to bind to the refval ref signature before attempting to overload the lvalue ref. The value of lvalue ref should never be associated with rvalue ref, unless it can be considered as the value of x (guaranteed destruction in the current area, whether we want it or not).

It is tempting to say that in the case of rvalue we know for sure that the object being transferred is temporary. This is not relevant. This is a signature designed to bind links to what seems like a temporary object.

For an analogy, as well as to do int * x = 23; - this may be wrong, but you can (eventually) force it to compile with poor results if you run it. The compiler cannot say for sure if you are serious about this or pull his leg.

In terms of security, you must consider the functions that do this (and why not do it - if it still compiles at all):

 A & make_A(void) { A new_a; return new_a; } 

While there is nothing supposedly wrong in the aspect of the language - types work, and we get a link to somewhere back, because new_a the storage location is inside the function, the memory will be returned / invalid when the function returns. Therefore, everything that uses the result of this function will deal with freed memory.

Similarly, A f( A && a ) intended, but not limited, to accept prvalues ​​or xvalues ​​if we really want to drive away something else. Which is where std::move comes in, and let's do just that.

The reason for this is that it differs from A f( A & a ) only in which contexts will be preferred over the rvalue overload . In all other respects, it is identical to how A handled by the compiler.

The fact that we know that A&& is a signature reserved for traffic is controversial; it is used to determine the version of the “reference to the A -type parameter” that we want to bind, the sort in which we must take ownership (rvalue) or the sort in which we must not take ownership (lvalue) of the underlying data (t .e. move it to another place and wipe the instance / link that we give). In both cases, what we are working with is a reference to a memory that is not controlled by f .

Whether we will do it or not, this is not what the compiler can say; it falls into the area of ​​"common sense" programming, for example, so as not to use memory locations that do not make sense to use, but otherwise are the correct memory cells.

What the compiler knows about A f( A && a ) is not to create a new repository for A , since we will be provided with an address (link) for work. We can leave the original address untouched, but the whole idea here is that by declaring A&& we tell the compiler “hey! Give me references to objects that are about to disappear so that I can do something before this happens " The key word here may be, and again the fact that we can explicitly configure this function signature incorrectly.

Consider whether we had version A , which when constructing the move did not delete the old instance data, and for some reason we did it by design (say, we had our own memory allocation functions and knew exactly how our memory model would store data during the lifetime of objects).

The compiler cannot know this, because to determine what happens to objects when they are processed in rvalue bindings, he will analyze the code, this is a question of human judgment at this moment. In the best case, the compiler sees "link, yay, without allocating extra memory here" and following the rules for passing links.

It is safe to assume that the compiler thinks: "This is a link, I do not need to deal with its lifetime in memory inside f , this temporary will be deleted after completion of f .

In this case, when the temporary is passed to f , the memory of this temporary file will disappear as soon as we leave f , and then we will potentially be in the same situation as A & make_A(void) - very bad.

The problem of semantics ...

std::move

The very purpose of std::move is to create rvalue links. By and large, what he does (if nothing else), this leads to the fact that the resulting value is tied to the values ​​of r, unlike lvalues. The reason for this is the reverse signature of A& before rvalue references are available, was ambiguous for things like operator overloads (and, of course, others).

Operators Example

 class A { // ... public: A & operator= (A & rhs); // what is the lifetime of rhs? move or copy intended? A & operator+ (A & rhs); // ditto // ... }; int main() { A result = A() + A(); // wont compile! } 

Please note that this will not accept temporary objects for any operator! It also makes no sense to do this in the case of copying objects - why do we need to change the original object that we copy, perhaps to have a copy that we can change later. It is for this reason that we must declare const A & parameters for copy operators and any situation where it is necessary to copy a link, as a guarantee that we do not modify the original object.

Naturally, this is a problem with moves, where we must change the original object in order to exclude premature release of new container data. (hence the operation "move").

To solve this problem, T&& announcements appear that replace the above sample code and, in particular, target objects in situations where the above will not be compiled. But we would not need to change operator+ as a move operation, and it would be difficult for you to find a reason for this (although you might think). Again, due to the assumption that the addition should not change the original object, only the left-operand object in the expression. Therefore, we can do this:

 class A { // ... public: A & operator= (const A & rhs); // copy-assign A & operator= (A && rhs); // move-assign A & operator+ (const A & rhs); // don't modify rhs operand // ... }; int main() { A result = A() + A(); // const A& in addition, and A&& for assign A result2 = A().operator+(A()); // literally the same thing } 

Here you should pay attention to the fact that, although A() returns a temporary value, it can not only bind to const A& , but should due to the expected semantics of addition (that it does not change its right operand). The second version of the assignment is clear why you should expect a change in only one of the arguments.

It is also clear that a transition will occur upon assignment, and no movement will occur with rhs in operator+ .

Separation of return semantics and argument binding semantics

The reason that there is only one step above is clear from the definitions of a function (well, an operator). The important thing is that we really associate what is explicitly the value of xvalue / rval with what is unmistakably the lvalue in operator+ .

I must emphasize this point: in this example there is no effective difference in how operator+ and operator= relate to their argument. As for the compiler, in any function body, the argument is effectively const A& for + and A& for = . The only difference is const ness. The only way A& and A&& differ is by distinguishing signatures, not types.

Different semantics come out with different signatures; this is a compiler tool for distinguishing certain cases when otherwise there is no clear difference from the code. The behavior of the functions themselves - the body of the code - may not be able to talk about cases separately!

Another example of this is operator++(void) vs operator++(int) . The first expects to return to its base value before the increase operation, and the second - subsequently. There is no skipping int , so the compiler has two signatures to work with - there is no other way to specify two identical functions with the same name, and as you may or may not know, this illegally overloads the function only for the return type for the same ambiguity reasons.

rvalue variables and other odd situations - exhaustive test

In order to unambiguously understand what is happening in f , I put together a buffet of the fact that one “should not try, but look as if they will work”, which greatly enhances the compiler’s hand on this issue:

 void bad (int && x, int && y) { x += y; } int & worse (int && z) { return z++, z + 1, 1 + z; } int && justno (int & no) { return worse( no ); } int num () { return 1; } int main () { int && a = num(); ++a = 0; a++ = 0; bad( a, a ); int && b = worse( a ); int && c = justno( b ); ++c = (int) 'y'; c++ = (int) 'y'; return 0; } 

g++ -std=gnu++11 -O0 -Wall -c -fmessage-length=0 -o "src\\basictest.o" "..\\src\\basictest.cpp"

 ..\src\basictest.cpp: In function 'int& worse(int&&)': ..\src\basictest.cpp:5:17: warning: right operand of comma operator has no effect [-Wunused-value] return z++, z + 1, 1 + z; ^ ..\src\basictest.cpp:5:26: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int' return z++, z + 1, 1 + z; ^ ..\src\basictest.cpp: In function 'int&& justno(int&)': ..\src\basictest.cpp:8:20: error: cannot bind 'int' lvalue to 'int&&' return worse( no ); ^ ..\src\basictest.cpp:4:7: error: initializing argument 1 of 'int& worse(int&&)' int & worse (int && z) { ^ ..\src\basictest.cpp: In function 'int main()': ..\src\basictest.cpp:16:13: error: cannot bind 'int' lvalue to 'int&&' bad( a, a ); ^ ..\src\basictest.cpp:1:6: error: initializing argument 1 of 'void bad(int&&, int&&)' void bad (int && x, int && y) { ^ ..\src\basictest.cpp:17:23: error: cannot bind 'int' lvalue to 'int&&' int && b = worse( a ); ^ ..\src\basictest.cpp:4:7: error: initializing argument 1 of 'int& worse(int&&)' int & worse (int && z) { ^ ..\src\basictest.cpp:21:7: error: lvalue required as left operand of assignment c++ = (int) 'y'; ^ ..\src\basictest.cpp: In function 'int& worse(int&&)': ..\src\basictest.cpp:6:1: warning: control reaches end of non-void function [-Wreturn-type] } ^ ..\src\basictest.cpp: In function 'int&& justno(int&)': ..\src\basictest.cpp:9:1: warning: control reaches end of non-void function [-Wreturn-type] } ^ 01:31:46 Build Finished (took 72ms) 

This is the unchanged header of the sans build assembly that you do not need to see :) I will leave it as an exercise to understand the errors found, but after reading my own explanations (especially later on), it should be obvious that every error was caused and why, imo in anyway.

Conclusion - What can we learn from this?

First, note that the compiler treats function bodies as separate units of code. This is basically the key. Regardless of what the compiler does with the function body, it cannot make assumptions about the behavior of the function, which requires that the function body be changed. There are templates to deal with these cases, but this is beyond the scope of this discussion - just note that the templates generate several function bodies to handle different cases, while otherwise the same function organ must be reused in each case when a function can be used.

Secondly, rvalue types were predominantly provided for relocation operations - a very specific circumstance that should have occurred during the assignment and construction of objects. Another semantics using rvalue reference bindings goes beyond any compiler that you have to deal with. In other words, it's better to think of rvalue references as syntactic sugar than the actual code. The signature is different from A&& vs A& , but the argument type is not for function body purposes, it is always considered as A& with the intention of passing the transmitted object in some way, because const A& , although correctly syntactically, will not allow the desired behavior.

I can be very sure of this when I say that the compiler will generate the code body for f , as if it were declared f(A&) . According to the above, A&& helps the compiler in deciding when to allow the binding of a mutable reference to f , but otherwise the compiler does not consider the semantics of f(A&) and f(A&&) be different from what f returned.

This is a long way to say: the return f method is independent of the type of argument it receives.

Confusion is an elite. In fact, there are two instances when returning a value. First, a copy is created as temporary, then this temporary is assigned to something (or it is not and remains purely temporary). The second copy is most likely eliminated by optimizing the return. The first copy can be moved to g and cannot be located to f . I expect that in a situation where f cannot be undone, a copy will appear and then the transition from f to the source code.

To override this, the temporary must be explicitly built using std::move , that is, in the return statement in f . However, in g we return what is known to be temporary for the body of the function g , so it either moves twice or moves once, and then is deleted.

, , , . , / , , , , , ...

+7
source

: doSomething .

: doSomething a , , f . rvalue .

: , doSomething a , undefined , return - g , , , rvalue

TL/DR: f g , doSomething . , f , g rvalue (, std::move ).

0
source

. . , , , , ?:) . , , , " ".

?

f g - . , , . - , , . .

References

First of all. ? ?

, . , . - , , . , , - , , , , (, ).

, " ", , , lvalue . lvalue. *pointer = 3 , * , .

, , , , , , ++, ( ++), - , .

, ' - :

 { A temp; temp.property = value; } 

temp . , . , , , - , :

 A & ref_to_temp = temp; // nope A * ptr_to_temp = &temp; // double nope 

, , , . , , , , ( , ).

, , , , , . , . , , LHS , . I.e:

 struct scopetester { static int counter = 0; scopetester(){++counter;} ~scopetester(){--counter;} }; scopetester(), std::cout << scopetester::counter; // prints 1 scopetester(), scopetester(), std::cout << scopetester::counter; // prints 2 

, ++i++ - undefined, (, i++ = ++i ). , .

: elision/in-place-construction (aka RVO) reference-assign-from-tempor.

Elision

? RVO ? , - " ". , . For instance:

 A x (void) {return A();} A y( x() ); 

, .

  • x
A , x() , A , A - y -

, A , . , ?

№1 - . , , . , .

# 2 . , . ( , ): NRVO RVO . , № 3, № 2...

quirk of elision :

Notes

elision - , . , (, ), , /, .

copy//move , ( ), .

( ++ 11)

return throw-expression, , , , , , lvalue; . .

:

Notes

/ , .

( ++ 11)

expression lvalue, , , expression , , , : , expression rvalue ( , , const), , , lvalue ( , ).

, expression ( )

. , /, , , . , - , , , " ".

, , , . , , , move/copy ? ? , , , : no - , , .

: , , . , , , - .

, , ( ) , .

-

, return - , catch catch, ( cv- ) , / . , , . NRVO, " ".

, - , ( cv- ), / . , , . return, RVO, " ".

, , :

 A & func() { A result; return result; } 

, - ( ?), . , - temp ? - result , func , - .

, , , result out of func - - . A* out.

- , , , . " " - , , , , , .

, .

, . , ( ) .

: . . , , , , - , . , ( ). - , , , .

, . , ( ), , ( , ).

, , - , , , , - .

, , , . , elision/RVO - , , , , .

, . How? , , , :

 class A { /* assume rule-of-5 (inc const-overloads) has been followed but unless * otherwise noted the members are private */ public: A (void) { /* ... */ } A operator+ ( const A & rhs ) { A res; // do something with `res` return res; } }; A x = A() + A(); // doesn't compile A & y = A() + A(); // doesn't compile A && z = A() + A(); // compiles 

Why? What's happening?

A x = ... - , .

A & y = ... - , , , .

A && z = ... - , x. , lvalue, lvalue. Sounds familiar? , . , , .

, , , res , . ( , , -std = gnu ++ 11, g++ 4.9.3).

, . -, , " " Type&& .

f g

, - , , () .

 A f( A && a ) { // has storage duration exceeding f scope. // already constructed. return a; // can be elided. // must be copy-constructed, a exceeds f scope. } A g( A a ) { // has storage duration limited to this function scope. // was just constructed somehow, whether by elision, move or copy. return a; // elision may occur. // can move-construct if can't elide. // can copy-construct if can't move. } 

, f A , , . f (prvalues), lvalue-, (xvalues) lvalue- ( xvalues ​​ std::move ), , f A A , , f . , f , f , , , , A .

g . A - - g . , , x. , , , , / A .

f

 // we can't tell these apart. // `f` when compiled cannot assume either will always happen. // case-by-case optimizations can only happen if `f` is // inlined into the final code and then re-arranged, or if `f` // is made into a template to specifically behave differently // against differing types. A case_1() { // prvalues return f( A() + A() ); } A make_case_2() { // xvalues A temp; return temp; } A case_2 = f( make_case_2() ) A case_3(A & other) { // lvalues return f( std::move( other ) ); } 

- , f . , A&& std::move .. f , , . - - , ( ) .

rvalue " ", , , , . - , , rvalue.

rvalue , , . , - .

, xvalue rvalue, , - , , .

, :

 A c = f( A() ); 

and this:

 A && r = f( A() ); 

, , c vs elided, r / - - , , r , .

A&&a , ?

Consider this:

 void bad_free(A && a) { A && clever = std::move( a ); // 'clever' should be the last reference to a? } 

This will not work. . A , rvalue , . clever , A , xvalue ( std::move , ..).

, , lvalues ​​ rvalues, , , , . lvalue , , , .

rvalues ​​ , , - . Consider:

 A r = f( A() ); // v1 A && s = f( A() ); // v2 

What's happening? f , , ( f ) - ( , ). v1 r - : move, copy, elide. v2 , f s , : s f , , f , , .

: v1 ( ), . v2 , / , , , / !

, , f . g :

 A r = g( A() ); // v1 A && s = g( A() ); // v2 

g , A() . f , xvalue, ( g ). , , v1 , (, ), v2 - , , t .

xvalue

, ( ):

 A && x (void) { A temp; // return temp; // even though xvalue, can't do this return std::move(temp); } A && y = x(); // y now refers to temp, which is destroyed 

y ? : y , temp , . , temp , y .

temp , A g / f ? - , : , , . , , , , , , .

If you want to eliminate all doubts, try passing this as a reference to rvalue: std::move(*(new A))- it should happen that nothing should destroy it, because it is not on the stack and because rvalue links do not change the lifetime of anything other than temporary objects (i.e. intermediate / expressions). xvalues ​​are candidates for structure / move moves and cannot be undone (already built), but all other move / copy operations can be theoretically excluded at the whim of the compiler; when using rvalue references, the compiler has no choice but to specify or pass an address.

0
source

All Articles