Why Is 'Std::Move' Named 'Std::Move'

Why is `std::move` named `std::move`?

It is correct that std::move(x) is just a cast to rvalue - more specifically to an xvalue, as opposed to a prvalue. And it is also true that having a cast named move sometimes confuses people. However the intent of this naming is not to confuse, but rather to make your code more readable.

The history of move dates back to the original move proposal in 2002. This paper first introduces the rvalue reference, and then shows how to write a more efficient std::swap:

template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}

One has to recall that at this point in history, the only thing that "&&" could possibly mean was logical and. No one was familiar with rvalue references, nor of the implications of casting an lvalue to an rvalue (while not making a copy as static_cast<T>(t) would do). So readers of this code would naturally think:

I know how swap is supposed to work (copy to temporary and then exchange the values), but what is the purpose of those ugly casts?!

Note also that swap is really just a stand-in for all kinds of permutation-modifying algorithms. This discussion is much, much bigger than swap.

Then the proposal introduces syntax sugar which replaces the static_cast<T&&> with something more readable that conveys not the precise what, but rather the why:

template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}

I.e. move is just syntax sugar for static_cast<T&&>, and now the code is quite suggestive as to why those casts are there: to enable move semantics!

One must understand that in the context of history, few people at this point really understood the intimate connection between rvalues and move semantics (though the paper tries to explain that as well):

Move semantics will automatically come into play when given rvalue
arguments. This is perfectly safe because moving resources from an
rvalue can not be noticed by the rest of the program (nobody else has
a reference to the rvalue in order to detect a difference
).

If at the time swap was instead presented like this:

template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}

Then people would have looked at that and said:

But why are you casting to rvalue?


The main point:

As it was, using move, no one ever asked:

But why are you moving?


As the years went on and the proposal was refined, the notions of lvalue and rvalue were refined into the value categories we have today:

Taxonomy

(image shamelessly stolen from dirkgently)

And so today, if we wanted swap to precisely say what it is doing, instead of why, it should look more like:

template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}

And the question everyone should be asking themselves is if the above code is more or less readable than:

template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}

Or even the original:

template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}

In any event, the journeyman C++ programmer should know that under the hood of move, nothing more is going on than a cast. And the beginner C++ programmer, at least with move, will be informed that the intent is to move from the rhs, as opposed to copy from the rhs, even if they don't understand exactly how that is accomplished.

Additionally, if a programmer desires this functionality under another name, std::move possesses no monopoly on this functionality, and there is no non-portable language magic involved in its implementation. For example if one wanted to code set_value_category_to_xvalue, and use that instead, it is trivial to do so:

template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}

In C++14 it gets even more concise:

template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}

So if you are so inclined, decorate your static_cast<T&&> however you think best, and perhaps you will end up developing a new best practice (C++ is constantly evolving).

So what does move do in terms of generated object code?

Consider this test:

void
test(int& i, int& j)
{
i = j;
}

Compiled with clang++ -std=c++14 test.cpp -O3 -S, this produces this object code:

__Z4testRiS_:                           ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc

Now if the test is changed to:

void
test(int& i, int& j)
{
i = std::move(j);
}

There is absolutely no change at all in the object code. One can generalize this result to: For trivially movable objects, std::move has no impact.

Now lets look at this example:

struct X
{
X& operator=(const X&);
};

void
test(X& i, X& j)
{
i = j;
}

This generates:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc

If you run __ZN1XaSERKS_ through c++filt it produces: X::operator=(X const&). No surprise here. Now if the test is changed to:

void
test(X& i, X& j)
{
i = std::move(j);
}

Then there is still no change whatsoever in the generated object code. std::move has done nothing but cast j to an rvalue, and then that rvalue X binds to the copy assignment operator of X.

Now lets add a move assignment operator to X:

struct X
{
X& operator=(const X&);
X& operator=(X&&);
};

Now the object code does change:

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc

Running __ZN1XaSEOS_ through c++filt reveals that X::operator=(X&&) is being called instead of X::operator=(X const&).

And that's all there is to std::move! It completely disappears at run time. Its only impact is at compile-time where it might alter what overload gets called.

Why is std::move generating instructions here?

You are compiling without optimisations. So you see exactly what is written without any attempt to simplify or inline functions.

Generated code is roughly eqivalent to what type&& foo(type& x) { return x; } would generate, which is what move does.

Studying assembly generated without optimisations turned on is exercise in futility.

Why is there a std::move in both algorithm and utility

So isn't it kind of confusing for both of these to be named move?

It can be confusing, especially those who are not used to languages that support overloading. It is true that programming guidelines typically discourage overloads with separate meanings.

But it is also not very difficult to learn that there are two functions by the same name, although this is subjective. The different argument lists provide sufficient context to easily recognise one from the other.

Why is there a std::move in both and

Because the designers of the language chose to use the same name for both functions.

std::move is a very concise way to express both functions. The one in <algorithm> is complementary to std::copy from the same header.

whereas the std::move in casts its argument to an xvalue, which is basically just a preprocessing step for eventually moving

Describing what the function does is not the only thing that the name of the function can express. In this case, the name expresses the intention of the programmer who used the function: The programmer intends to move from the argument lvalue - if possible.

This is something that programmers may potentially need to write quite often; thus there is a need for very short name.

What does std::move do when called on a function?

Look carefully at A's constructor:

A::A (std::function<void(Timer *, const TimerContext&)> cb)

The function, cb is being passed by value. That means a copy of the function has already occurred from when it was invoked via new:

A *a = new A{customCallback};

The std::move in the constructor initializer list exists to avoid a redundant copy into the member variable. The original function defined by the caller who invoked new A remains unmoved. This is preferred because copying a std::function variable can be expensive. (sizeof(cb) - might be way bigger than you expected).

An alternative implementation: The function could have been passed as a const reference and allow the copy to occur in the constructor itself:

A::A (const std::function<void(Timer *, const TimerContext&)>& cb) : callback_{cb}

What happens when std::move() is called without assignment

std::move() doesn't move the passed object by itself; it just possibly enables move semantics for the object. It consists of a cast whose result you can use to select – if possible – the proper overload that supports move semantics. Therefore, in your code:

std::string s = "Moving";
std::move(s);

std::move(s) above casts s to an rvalue reference. The result of the casting is not used. The object s above is not actually moved, and it isn't modified.

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

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

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.

When do you need to explicitly call std::move and when not in cpp?

When do you need to explicitly call std::move and when not in cpp?

In short, and technically precise words: Use std::move when you have an lvalue that you want to be an rvalue. More practically: You would want to do that when there is a copy that you want instead to be a move. Hence the name std::move.

In the example, you return an automatic variable. There is no copy that can be avoided by using std::move because in the special case of returning an automatic variable, there will be a move even from an lvalue.

Here I show the move constructor and the function that I thought would use it. It doesn't.

Just because there is a move in the abstract machine, doesn't necessarily mean that there would be a call to the move constructor. This is a good thing because doing nothing can potentially be faster than calling the move constructor.

This is known as (Named) Return Value Optimization. Or more generally copy elision. Using std::move inhibits this optimization, so not only is it unnecessary in this case, but it is also counter productive.



Related Topics



Leave a reply



Submit