简体   繁体   中英

Why is static_cast<Object&&> necessary in this function?

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", the static_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.

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

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.

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.

Forwarding references are a special kind of reference syntax designed to preserve the value category of a function argument. Ie non-template function

Object&& move(Object&& arg)

is not equal to std::move for Object , which declared something like (c++11):

template<class T>
std::remove_reference<T>::type&& move( T&& t );

In non-template function arg is an lvalue, in template it have same value category as expression used to initialize it. In template std::remove_reference<T>::type refers to T, so std::remove_reference<T>::type&& is a true rvalue reference to T - a way around T&& alternative meaning.

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, ie 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&) .

Template std::move is type-correct "wrapper" around the static_cast to facilitate "implicit" cast, to simplify code by removing repeated static_cast with explicit type from code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM