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

13,226

Solution 1

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.

Solution 2

Let me just leave here a quote from the C++11 FAQ written by B. Stroustrup, which is a direct answer to OP's question:

move(x) means "you can treat x as an rvalue". Maybe it would have been better if move() had been called rval(), but by now move() has been used for years.

By the way, I really enjoyed the FAQ - it's worth reading.

Share:
13,226
Howard Hinnant
Author by

Howard Hinnant

http://howardhinnant.github.io https://github.com/HowardHinnant

Updated on June 12, 2022

Comments

  • Howard Hinnant
    Howard Hinnant almost 2 years

    The C++11 std::move(x) function doesn't really move anything at all. It is just a cast to r-value. Why was this done? Isn't this misleading?

  • Xeo
    Xeo over 10 years
    While it's nice that we can give it another name, the rest of the world won't recognize it. :/
  • sehe
    sehe over 10 years
    Here's a dot source for that graph: I recreated it digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } } for the public good :) Download it here as SVG
  • dyp
    dyp over 10 years
    Is this still open to bikeshedding? I'd suggest allow_move ;)
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 10 years
    set_value_category_to_xvalue is far easier to write than that. template<class T> constexpr auto set_value_category_to_xvalue(T&& t) noexcept -> decltype(std::move(t)) { return std::move( t ); } ;)
  • Admin
    Admin over 10 years
    @Yakk noexcept(noexcept(std::move(t))) to avoid reinventing the wheel. :)
  • Daniel Frey
    Daniel Frey over 10 years
    Just for the history section: In Mojo it was called as_temporary.
  • Daniel Frey
    Daniel Frey over 10 years
    @dyp My favorite is still movable.
  • nairware
    nairware over 10 years
    Scott Meyers suggested renaming std::move to rvalue_cast: youtube.com/…
  • Howard Hinnant
    Howard Hinnant over 10 years
    Since rvalue now refers to both prvalues and xvalues, rvalue_cast is ambiguous in its meaning: what kind of rvalue does it return? xvalue_cast would be a consistent name here. Unfortunately, most people, at this time, would also not understand what it is doing. In a few more years, my statement will hopefully become false.
  • Johannes Schaub - litb
    Johannes Schaub - litb over 9 years
    and here is the google charts url for that DOT diagram: click
  • Andy
    Andy almost 9 years
    I haven't really grokked all of this...what is the compiled machine code doing differently when you do a = static_cast<T&&>(b) rather than a = b? Is it somehow swapping what the references themselves point to instead of invoking operator =, or what?
  • Nawaz
    Nawaz almost 9 years
    std::enable_move would have been a better name, or std::try_move, or std::make_moveable?
  • Howard Hinnant
    Howard Hinnant almost 9 years
    @Nawaz: So write it for yourself. Just follow the recipe I've given for set_value_category_to_xvalue.
  • Nawaz
    Nawaz almost 9 years
    @HowardHinnant: No, I'm used to std::move by now. I was thinking loud (like other guys here), even though I knew it cannot be renamed.
  • Howard Hinnant
    Howard Hinnant about 8 years
    @Andy: After only 2 years, I've updated the answer to address your question. ;-)
  • einpoklum
    einpoklum about 8 years
    To plagiarize @HowardHinnant's comment from another answer: Stroustrup answer is inaccurate, as there are now two kinds of rvalues - prvalues and xvalues, and std::move is really an xvalue cast.
  • Andy
    Andy about 8 years
    @HowardHinnant oh, okay, so basically since it produces the type X&&, if there is an overload for the function being called (operator = in this case, but I assume it could be any function) that takes type X&&, it calls that overload instead of one that takes X&, and if there is no overload that takes X&, the X&& gets implicitly cast back to X& and that overload is called. Right?
  • Howard Hinnant
    Howard Hinnant about 8 years
    @Andy: Pretty close. An rvalue X can't bind to X& (except on VS), but can bind to a const X&, and will do so exactly as you describe.
  • Andy
    Andy about 8 years
    @HowardHinnant oh, right, that makes sense. Thanks so much for the explanation!