How does std::move convert expressions to rvalues?
Asked Answered
C

2

126

I don't fully understand the implementation of std::move(). Namely, I am confused by this implementation in the MSVC standard library:

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
move(_Ty&& _Arg)
{ // forward _Arg as movable
    return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
}

When I call std::move like this:

Object obj1;
Object obj2 = std::move(obj1); // _Ty&& _Arg binds to obj1

... the _Arg reference parameter binds to the lvalue obj1. You cannot directly bind an rvalue reference to an lvalue, which makes me think that a cast to an rvalue reference like (Object&&) is required. However, this is absurd because std::move() must work for all the values.

To fully understand how this works, I've looked at the implementation of std::remove_reference too:

template<class _Ty>
struct _Remove_reference
{ // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{ // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{ // remove rvalue reference
    typedef _Ty _Type;
};

Unfortunately it's still as confusing and I don't get it. Please help me understand the implementation of std::move.

Cacilie answered 22/9, 2011 at 6:0 Comment(3)
@NicolBolas On the contrary, the question itself shows that OP is underselling themself in that statement. It's precisely OP's grasp of basic syntax skills that allows them to even pose the question.Mckenney
I disagree anyway - you can learn quickly or already have a good grasp of computer science or programming as a whole, even in C++, and still struggle with the syntax. It's better to spend your time learning mechanics, algorithms, etc. but rely on references to brush up on syntax as needed, than it is to be technically articulate but to have a shallow understanding of things like copy/move/forward. How are you supposed to know "when and how to use std::move when you want to move construct something" before you know what move does?Highpowered
I think I came here to understand how move works rather than how it's implemented. I find this explanation really useful: pagefault.blog/2018/03/01/….Mercuri
U
195

We start with the move function (which I cleaned up a little bit):

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

Let's start with the easier part - that is, when the function is called with rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

and our move template gets instantiated as follows:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

Since remove_reference converts T& to T or T&& to T, and Object is not reference, our final function is:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

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).


Here's what happens when we call move with lvalue:

Object a; // a is lvalue
Object b = std::move(a);

and corresponding move instantiation:

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Again, remove_reference converts Object& to Object and we get:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

Now we get to the tricky part: what does Object& && even mean and how can it bind to lvalue?

To allow perfect forwarding, C++11 standard provides special rules for reference collapsing, which are as follows:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

As you can see, under these rules Object& && actually means Object&, which is plain lvalue reference that allows binding lvalues.

Final function is thus:

Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

which is not unlike the previous instantiation with rvalue - they both cast its argument to rvalue reference and then return it. The difference is that first instantiation can be used with rvalues only, while the second one works with lvalues.


To explain why do we need remove_reference a bit more, let's try this function

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

and instantiate it with lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

Applying the reference collapsing rules mentioned above, you can see we get function that is unusable as move (to put it simply, you call it with lvalue, you get lvalue back). If anything, this function is the identity function.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Underlaid answered 22/9, 2011 at 16:39 Comment(12)
Nice answer. Although I understand that for an lvalue it is a good idea to evaluate T as Object&, I didn't know that this is really done. I would have expected T to also evaluate to Object in this case, as I thought this was the reason for introducing wrapper references and std::ref, or wasn't it.Clerihew
Is this Wikipedia article outdated/misinformed or is there a hidden difference I have not realized?Clerihew
There's a difference between template <typename T> void f(T arg) (which is what is in the Wikipedia article) and template <typename T> void f(T& arg). The first one resolves to value (and if you want to pass reference, you have to wrap it in std::ref), while the second one always resolves to reference. Sadly, rules for template argument deduction are rather complex, so I cannot provide precise reasoning why T&& resovles to Object& && (but it happens indeed).Underlaid
This is extremely well done explaining why you must cast. From reading some of the proposals and draft standards you would not pick that up. For example the demo implementation of move at open-std.org/jtc1/sc22/wg21/docs/papers/2006/… lacks the cast.Separation
@nixeagle: The reason is that the proposal for rvalue references went through quite a few changes. If I'm not mistaken, the rules allowed to omit the cast at some point.Underlaid
@MarkIngram: I really meant "prvalue". :) It's a techincal term defined by the C++ standard, see stackoverflow.com/questions/3601602 .Underlaid
But, is there any reason that this direct approach would not work? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }Fascism
That explains why remove_reference is necessary, but I still don't get why the function has to take T&& (instead of T&). If I understand this explanation correctly, it shouldn't matter whether you get a T&& or T& because either way, remove_reference will cast it to T, then you'll add back the &&. So why not say you accept a T& (which semantically is what you're accepting), instead of T&& and relying on perfect forwarding to allow the caller to pass a T&?Bessiebessy
@mgiuca: If you want std::move to only cast lvalues to rvalues, then yes, T& would be okay. This trick is done mostly for flexibility: you can call std::move on everything (rvalues included) and get rvalue back.Underlaid
As additional note (if T is a template parameter), T&& is called "the universal reference", since it binds everything.Adherence
The types may work out, but I was really stuck on what it means to move something in the first place. It's a misnomer, isn't it? Nothing moves when you create a reference to an existing rvalue or creating a new rvalue to refer to, etc. - the original lvalue or rvalue is still in the scope it started in, has the same address as before, etc. What would moving (the bytes of) a value mean if not copying them to their destination?Highpowered
I wish I could un-see this. lolBreakthrough
C
4

_Ty is a template parameter, and in this situation

Object obj1;
Object obj2 = std::move(obj1);

_Ty is type "Object &"

which is why the _Remove_reference is necessary.

It would be more like

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

If we didn't remove the reference it would be like we were doing

Object&& obj2 = (ObjectRef&&)obj1_ref;

But ObjectRef&& reduces to Object &, which we couldn't bind to obj2.

The reason it reduces this way is to support perfect forwarding. See this paper.

Cisterna answered 22/9, 2011 at 6:12 Comment(2)
This doesn't explain everything about why the _Remove_reference_ is necessary. For instance, if you have Object& as a typedef, and you take a reference to it, you still get Object&. Why doesn't that work with &&? There is an answer to that, and it has to do with perfect forwarding.Argueta
True. And the answer is very interesting. A & && is reduced to A &, so if we tried to use (ObjectRef &&)obj1_ref, we would get Object & instead in this case.Cisterna

© 2022 - 2024 — McMap. All rights reserved.