A C++Next blog post said that
A compute(…)
{
A v;
…
return v;
}
If A
has an accessible copy or move constructor, the compiler may choose to elide the copy. Otherwise, if A
has a move constructor, v
is moved. Otherwise, if A
has a copy constructor, v
is copied.
Otherwise, a compile time error is emitted.
I thought I should always return the value without std::move
because the compiler would be able to figure out the best choice for users. But in another example from the blog post
Matrix operator+(Matrix&& temp, Matrix&& y)
{ temp += y; return std::move(temp); }
Here the std::move
is necessary because y
must be treated as an lvalue inside the function.
Ah, my head almost blow up after studying this blog post. I tried my best to understand the reasoning but the more I studied, the more confused I became. Why should we return the value with the help of std::move
?
Returning a value from a function will turn that value into an rvalue. Once you call return on an object, the name of the object does not exist anymore (it goes out of scope), so it becomes an rvalue. Similarly, calling a function will give back an rvalue. The return value of a function has no name.
std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t . It is exactly equivalent to a static_cast to an rvalue reference type.
std::move itself does "nothing" - it has zero side effects. It just signals to the compiler that the programmer doesn't care what happens to that object any more. i.e. it gives permission to other parts of the software to move from the object, but it doesn't require that it be moved.
Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higer performance and more robust libraries.
So, lets say you have:
A compute()
{
A v;
…
return v;
}
And you're doing:
A a = compute();
There are two transfers (copy or move) that are involved in this expression. First the object denoted by v
in the function must be transferred to the result of the function, i.e. the value donated by the compute()
expression. Let's call that Transfer 1. Then, this temporary object is transferred to create the object denoted by a
- Transfer 2.
In many cases, both Transfer 1 and 2 can be elided by the compiler - the object v
is constructed directly in the location of a
and no transferring is necessary. The compiler has to make use of Named Return Value Optimization for Transfer 1 in this example, because the object being returned is named. If we disable copy/move elision, however, each transfer involves a call to either A's copy constructor or its move constructor. In most modern compilers, the compiler will see that v
is about to be destroyed and it will first move it into the return value. Then this temporary return value will be moved into a
. If A
does not have a move constructor, it will be copied for both transfers instead.
Now lets look at:
A compute(A&& v)
{
return v;
}
The value we're returning comes from the reference being passed into the function. The compiler doesn't just assume that v
is a temporary and that it's okay to move from it1. In this case, Transfer 1 will be a copy. Then Transfer 2 will be a move - that's okay because the returned value is still a temporary (we didn't return a reference). But since we know that we've taken an object that we can move from, because our parameter is an rvalue reference, we can explicitly tell the compiler to treat v
as a temporary with std::move
:
A compute(A&& v)
{
return std::move(v);
}
Now both Transfer 1 and Transfer 2 will be moves.
1 The reason why the compiler doesn't automatically treat v
, defined as A&&
, as an rvalue is one of safety. It's not just too stupid to figure it out. Once an object has a name, it can be referred to multiple times throughout your code. Consider:
A compute(A&& a)
{
doSomething(a);
doSomethingElse(a);
}
If a
was automatically treated as an rvalue, doSomething
would be free to rip its guts out, meaning that the a
being passed to doSomethingElse
may be invalid. Even if doSomething
took its argument by value, the object would be moved from and therefore invalid in the next line. To avoid this problem, named rvalue references are lvalues. That means when doSomething
is called, a
will at worst be copied from, if not just taken by lvalue reference - it will still be valid in the next line.
It is up to the author of compute
to say, "okay, now I allow this value to be moved from, because I know for certain that it's a temporary object". You do this by saying std::move(a)
. For example, you could give doSomething
a copy and then allow doSomethingElse
to move from it:
A compute(A&& a)
{
doSomething(a);
doSomethingElse(std::move(a));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With