Trying to understand std::move
, I found this answer to another question.
Say I have this function
Object&& move(Object&& arg)
{
return static_cast<Object&&>(arg);
}
What I think I understand:
arg
is an lvalue (value category).arg
is of type "rvalue ref to Object".static_cast
converts types.arg
and the return type both being of type "rvalue ref to Object", thestatic_cast
is unnecessary.
However, the linked answer says:
Now, you might wonder: do we even need the cast? The answer is: yes, we do. The reason is simple; named rvalue reference is treated as lvalue (and implicit conversion from lvalue to rvalue reference is forbidden by standard).
I still don't understand why the static_cast is necessary given what I said above.
CodePudding user response:
the static_cast is unnecessary.
It may seem so, but it is necessary. You can find out easily by attempting to write such function without the cast, as the compiler should tell you that the program is ill-formed. The function (template) returns an rvalue reference. That rvalue reference cannot be bound to an lvalue. The id-expression arg
is an lvalue (as you stated) and hence the returned rvalue reference cannot be bound to it.
It might be easier to understand outside of return value context. The rules are same here:
T obj;
T&& rref0 = obj; // lvalue; not OK
T&& rref1 = static_cast<T&&>(obj); // xvalue; OK
T&& rref2 = rref1; // lvalue; not OK
T&& rref3 = static_cast<T&&>(rref1); // xvalue; OK
CodePudding user response:
Your first bullet is incorrect fundamentally, arg
is not a lvalue, neither it is an rvalue. Neither it's a rvalue or lvalue reference, because std::move
is a template. In template context a function argument of type T&&
is a forwarding reference, if T
is a template-parameter. Forwarding reference becomes of type which appropriate, depending on what is T.
(and implicit conversion from lvalue to rvalue reference is forbidden by standard).
A cast is required literally because of that. Following code is incorrect, because you can't call foo(v)
, as v
is a named object and it can be an lvalue:
void foo(int && a) { a = 5; }
int main()
{
int v;
foo(v);
std::cout << a << std::endl;
}
But if foo()
is a template, it may become a function with int&
, const int&
and int&&
arguments.
template<class T>
void foo(T && a) { a = 5; }
You would be able to call foo(v 5)
, where argument is a temporary, which can be bound to rvalue reference. foo
will change the temporary object which stops to exist after function call. That's the exact action which move constructors usually have to do - to modify temporary object before its destructor is called.
NB: An rvalue argument would cease to exist earlier , either after its use or at end of function call.
By analogy to function call above, if implicit conversion was possible, then it would be possible to call move constructor where copy constructor is appropriate but missing, i.e. by mistake. return static_cast<Object&&>(arg);
results in initialization involving call to Object::Object(Object&&)
by definition of return
, return arg
would call Object::Object(const Object&)
.
CodePudding user response:
I have the following mental model for it (let's use int
instead of Object
).
Objects which have a name are "sitting on the ground". They are lvalues; you cannot convert them to rvalue references.
int do_stuff(int x, int&& y) {...} // both x and y have a name
When you do calculations, you pick objects from the ground, do your stuff in mid-air and put the result back.
x y; // it's in mid-air
do_stuff(4, 5); // return value is in mid-air
These temporary results can be converted to rvalue references. But as soon as you "put them onto the ground", they behave as lvalues.
int&& z = x y; // on the ground
int&& z = do_stuff(6, 7); // on the ground
I am sure it only helps in simple situations, but at least it gives some real-world analogy to how C works.