How Does Std::Move() Transfer Values into Rvalues

How does std::move() transfer values into RValues?

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

Why does std::move copy contents for a rvalue or const lvalue function argument?

Your vector is actually copied, not moved. The reason for this is, although declared as an rvalue reference, vec_ denotes an lvalue expression inside the function body. Thus the copy constructor of std::vector is invoked, and not the move constructor. The reason for this is, that vec_ is now a named value, and rvalues cannot have names, so it collapses to an lvalue. The following code will fail to compile because of this reason:

void foo(int&& i)
{
int&& x = i;
}

In order to fix this issue, you have to make vec_ nameless again, by calling std::move(vec_).

Pass by value/reference/rvalue with a std::move(str) arg

When i is pass-by-value, d becomes empty,

To be accurate, d will be in some valid state not specified in the C++ standard. Empty is one possibility.

std::move itself never causes the move constructor to be called directly. Neither does binding an rvalue reference to an object cause move constructor to be called directly.

Only initialising an object with a non-const rvalue will cause the argument to be moved from. In the example, std::string i is initialised with a non-const rvalue and the move constructor will be called.

As an aside why does the code in the current state compile if d is cast to an x-value?

Because the type has a (non-deleted) move constructor. Therefore the argument can be initialised from an rvalues.

I had thought if we had std::string i, a copy of the rvalue reference is made.

std::string i is not a reference. It is a variable of type std::string and as such there is an object of type std::string associated with the variable. That object is initialised with the expression that is passed into the function as argument.

Also, if I observe that the output of d is still the same as prior to applying std::move, what does this mean in this case?

If you call the uncommented version of the function with an rvalue, then the argument will be moved from. If the value is same as it was, then it simply means that the value is the same. You cannot assume that the value will be the same nor that it won't be the same.

Does it mean that d is still occupying the space it originally occupied?

Assuming that by "space" you mean the storage where the variable is, then of course it is still occupying the same storage. The address of an object never changes through the lifetime of the object.

Reason to use std::move on rvalue reference parameter

isn't the std::move here unnecessary?

No. Types and value categories are different things.

(emphasis mine)

Each C++ expression (an operator with its operands, a literal, a variable name, etc.) is characterized by two independent properties: a type and a value category.

The following expressions are lvalue expressions:

the name of a variable, a function, a template parameter object (since
C++20), or a data member, regardless of type, such as std::cin or
std::endl. Even if the variable's type is rvalue reference, the
expression consisting of its name is an lvalue expression
;

std::move converts lvalue to rvalue (xvalue). As a named variable, x is an lvalue, std::move converts it to rvalue in objects[size++] = std::move(x); then the move assignment operator is supposed to be used. Otherwise, copy assignment operator will be used instead; lvalue can't be bound to rvalue reference.

we still need to use std::move on its member if we want to call move instead of copy right?

Yes, same reason as above.

What happens when std::move is called on a rvalue reference?

string constructed = s;

This does not cause a move because s is not an rvalue. It is an rvalue reference, but not an rvalue. If it has a name, it is not an rvalue. Even if the variable's type is rvalue reference, the expression consisting of its name is an lvalue expression.

string constructed = std::move(s);

This causes a move because std::move(s) is an rvalue: it's a temporary and its type is not lvalue reference.

There are no other moves in the program (std::move is not a move, it's a cast).

Advantages of pass-by-value and std::move over pass-by-reference

  1. Did I understand correctly what is happening here?

Yes.


  1. Is there any upside of using std::move over passing by reference and just calling m_name{name}?

An easy to grasp function signature without any additional overloads. The signature immediately reveals that the argument will be copied - this saves callers from wondering whether a const std::string& reference might be stored as a data member, possibly becoming a dangling reference later on. And there is no need to overload on std::string&& name and const std::string& arguments to avoid unnecessary copies when rvalues are passed to the function. Passing an lvalue

std::string nameString("Alex");
Creature c(nameString);

to the function that takes its argument by value causes one copy and one move construction. Passing an rvalue to the same function

std::string nameString("Alex");
Creature c(std::move(nameString));

causes two move constructions. In contrast, when the function parameter is const std::string&, there will always be a copy, even when passing an rvalue argument. This is clearly an advantage as long as the argument type is cheap to move-construct (this is the case for std::string).

But there is a downside to consider: the reasoning doesn't work for functions that assign the function argument to another variable (instead of initializing it):

void setName(std::string name)
{
m_name = std::move(name);
}

will cause a deallocation of the resource that m_name refers to before it's reassigned. I recommend reading Item 41 in Effective Modern C++ and also this question.

Why does std::move take rvalue reference as argument?

It's not an rvalue reference, but a forwarding reference; which could preserve the value category of the argument. That means std::move could take both lvalue and rvalue, and convert them to rvalue unconditionally.

Forwarding references are a special kind of references that preserve the value category of a function argument, making it possible to forward it by means of std::forward. Forwarding references are either:

1) function parameter of a function template declared as rvalue
reference to cv-unqualified type template parameter of that same
function template:

2) auto&& except when deduced from a brace-enclosed initializer list.

On the other hand, int&& is an rvalue reference; note the difference here, if a function template parameter has type T&& with template parameter T, i.e. a deduced type T, the parameter is a forwarding reference.

Does std::move called on a prvalue deconstruct the object?

std::move is a function call; it's not a magical operator of C++. All it does is return a reference to the object it is given. And it takes its parameter by reference.

In C++, if you have a prvalue, and you pass it to a function that takes a reference to it, the temporary created by that prvalue will only persist until the end of the expression.

So move returns a reference to the temporary. But that temporary will be destroyed immediately after myObj1 is initialized. So myObj1 is a reference to a destroyed object.

Understanding std::move and its purpose by example

Move constructors (or move assignments) provide the means for your program to avoid spending excessive time copying the contents of one object to another, if the copied-from object will no longer be used afterwards. For example, when an object is to be the returned value of a method or function.

This is more obvious when you have an object with, say, dynamically allocated content.

Example:

class MyClass {
private:
size_t _itemCount ;
double * _bigBufferOfDoubles ;
public:

// ... Initial contructor, creating a big list of doubles
explicit MyClass( size_t itemCount ) {
_itemCount = itemCount ;
_bigBufferOfDoubles = new double[ itemCount ];
}

// ... Copy constructor, to be used when the 'other' object must persist
// beyond this call
MyClass( const MyClass & other ) {

//. ... This is a complete copy, and it takes a lot of time
_itemCount = other._itemCount ;
_bigBufferOfDoubles = new double[ _itemCount ];
for ( int i = 0; i < itemCount; ++i ) {
_bigBufferOfDoubles[ i ] = other. _bigBufferOfDoubles[ i ] ;
}

}

// ... Move constructor, when the 'other' can be discarded (i.e. when it's
// a temp instance, like a return value in some other method call)
MyClass( MyClass && other ) {

// ... Blazingly fast, as we're just copying over the pointer
_itemCount = other._itemCount ;
_bigBufferOfDoubles = other._bigBufferOfDoubles ;

// ... Good practice to clear the 'other' as it won't be needed
// anymore
other._itemCount = 0 ;
other._bigBufferOfDoubles = null ;
}
~MyClass() {
delete [] _bigBufferOfDoubles ;
}
};

// ... Move semantics are useful to return an object 'by value'
// Since the returned object is temporary in the function,
// the compiler will invoke the move constructor of MyClass
MyClass someFunctionThatReturnsByValue() {
MyClass myClass( 1000000 ) ; // a really big buffer...
return myClass ; // this will call the move contructor, NOT the copy constructor
}

// ... So far everything is explicitly handled without an explicit
// call to std::move
void someOtherFunction() {
// ... You can explicitly force the use of move semantics
MyClass myTempClass( 1000000 ) ;

// ... Say I want to make a copy, but I don't want to invoke the
// copy contructor:
MyClass myOtherClass( 1 ) ;
myOtherClass = std::move( myTempClass ) ;

// ... At this point, I should abstain from using myTempClass
// as its contents have been 'transferred' over to myOtherClass.

}

How does std::move invalidates the value of original variable?

Depending on the implementation, the std::move could be a simple swap of the internal memory addresses.

If you run the following code on http://cpp.sh/9f6ru

#include <iostream>
#include <string>

int main()
{
std::string str1 = "test";
std::string str2 = "test2";

std::cout << "str1.data() before move: "<< static_cast<const void*>(str1.data()) << std::endl;
std::cout << "str2.data() before move: "<< static_cast<const void*>(str2.data()) << std::endl;

str2 = std::move(str1);
std::cout << "=================================" << std::endl;

std::cout << "str1.data() after move: " << static_cast<const void*>(str1.data()) << std::endl;
std::cout << "str2.data() after move: " << static_cast<const void*>(str2.data()) << std::endl;
}

You will get the following output:

str1.data() before move: 0x363d0d8
str2.data() before move: 0x363d108
=================================
str1.data() after move: 0x363d108
str2.data() after move: 0x363d0d8

But the result may vary depending on the implementation of the compiler and the std library.

But the implementation details can be even more complex http://cpp.sh/6dx7j. If you look at your example, then you will see that creating a copy for a string does not necessarily require that new memory for its content is allocated. This is because nearly all operations on std::string are read only or require the allocation of memory. So the implementation can decide to do just shallow copies:

#include <iostream>
#include <string>
#include <vector>

int main()
{
std::string str = "Hello";
std::vector<std::string> v;

std::cout << "str.data() before move: "<< static_cast<const void*>(str.data()) << std::endl;

v.push_back(str);
std::cout << "============================" << std::endl;
std::cout << "str.data() after push_back: "<< static_cast<const void*>(str.data()) << std::endl;
std::cout << "v[0].data() after push_back: "<< static_cast<const void*>(v[0].data()) << std::endl;

v.push_back(std::move(str));
std::cout << "============================" << std::endl;

std::cout << "str.data() after move: "<< static_cast<const void*>(str.data()) << std::endl;
std::cout << "v[0].data() after move: "<< static_cast<const void*>(v[0].data()) << std::endl;
std::cout << "v[1].data() after move: "<< static_cast<const void*>(v[1].data()) << std::endl;
std::cout << "After move, str is \"" << str << "\"\n";

str = std::move(v[1]);
std::cout << "============================" << std::endl;
std::cout << "str.data() after move: "<< static_cast<const void*>(str.data()) << std::endl;
std::cout << "v[0].data() after move: "<< static_cast<const void*>(v[0].data()) << std::endl;
std::cout << "v[1].data() after move: "<< static_cast<const void*>(v[1].data()) << std::endl;
std::cout << "After move, str is \"" << str << "\"\n";
}

The output is

str.data() before move: 0x3ec3048
============================
str.data() after push_back: 0x3ec3048
v[0].data() after push_back: 0x3ec3048
============================
str.data() after move: 0x601df8
v[0].data() after move: 0x3ec3048
v[1].data() after move: 0x3ec3048
After move, str is ""
============================
str.data() after move: 0x3ec3048
v[0].data() after move: 0x3ec3048
v[1].data() after move: 0x601df8
After move, str is "Hello"

And if you take a look at:

#include <iostream>
#include <string>
#include <vector>

int main()
{
std::string str = "Hello";
std::vector<std::string> v;

std::cout << "str.data() before move: "<< static_cast<const void*>(str.data()) << std::endl;

v.push_back(str);
std::cout << "============================" << std::endl;
str[0] = 't';
std::cout << "str.data() after push_back: "<< static_cast<const void*>(str.data()) << std::endl;
std::cout << "v[0].data() after push_back: "<< static_cast<const void*>(v[0].data()) << std::endl;

}

Then you would assume that str[0] = 't' would just replace the data in place. But this is not necessarily the case http://cpp.sh/47nsy.

str.data() before move: 0x40b8258
============================
str.data() after push_back: 0x40b82a8
v[0].data() after push_back: 0x40b8258

And moving primitives like:

void test(int i) {
int x=i;
int y=std::move(x);
std::cout<<x;
std::cout<<y;
}

Would be mostly be optimized out completely by the compiler:

  mov ebx, edi
mov edi, offset std::cout
mov esi, ebx
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov edi, offset std::cout
mov esi, ebx
pop rbx
jmp std::basic_ostream<char, std::char_traits<char> >::operator<<(int) # TAILCALL

Both std::cout used the same register, the x and y are completely optimized away.



Related Topics



Leave a reply



Submit