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