In C++, the value category of an expression is determined by two independent properties:
(References: cppreference and Stroustrup's '“New” Value Terminology"' paper)
I haven't found an explicit and easy-to-understand explanation of what "can be moved from" means exactly.
I have three questions:
What exactly does "can be moved from" mean?
Is this purely an abstract concept for expression classification, or does it imply some practical behavior at the hardware, code generation, or runtime level?
In the following code, what makes std::move(x)
"movable from" while x
itself is not?
auto x = 42;
auto rref = std::move(x);
I'm specifically asking in the context of C++11 (let's leave C++17's changes—like prvalue materialization—for another question). Also, please clarify which aspects are defined conceptually at the standard level and which are compiler optimizations.
"can be moved from" is politeness. It is impolite to move from things that aren't rvalues or rvalue references.
That's it.
The person making the expression can call std::move
to signal "I don't mind this being moved from". The overload set of parts of the expression can access if it was passed an rvalue (be it a prvalue or the result of something like std::move
), and decide if it wants to move anything.
I'll briefly mention c++17, as I think it is useful for context:
In c++17, there is also the idea of elision and guaranteed elision. std::move
never permits this.
I find Elision is best thought of as combining of lifetimes of objects. So:
Foo func() { return Foo{}; }
Foo var = func();
here we have 3 objects - the temporary object Foo{}
within func
, the return value of func
, and the Foo var
.
What elision does is permit there only be one actual object. The Foo{}
expression directly initializes the var
.
What more,
template<std::size_t N>
Foo func() { return func<N-1>(); }
template<>
Foo func<0> { return Foo{}; }
Foo var = func<1000>();
here we just elided the lifetime of 1003 variables together.
Now, back in c++11, we have elision without guaranteed elision. Under it the compiler is permitted to do the above transformation so long as certain requirements exist. These include that certain copy constructors exist. When the compiler does elision, it skips the copy constructor and merges the lifetimes of the objects.
So the compiler is permitted to skip the copy and move ctor here:
Foo func() { Foo f; return f; }
Foo var = func();
it must first verify that it exists, but it need not run the code. This does not require an as-if proof by the compiler; if the copy or move constructor calls exit()
, the compiler can still skip it.
Calling std::move
on the object blocks this elision (and guaranteed elision) in every case. Elision of named variables (also known as NRVO) is also somewhat fragile and difficult to guarantee.
Finally, there are various rules about when the compiler is required to use move construction on return. These have generally become more generous to "call the move constructor" as the standard versions have advanced.
Something like this:
Foo func( Foo&& var ) { return var; }
in the more recent versions of C++ will move, but back in c++11 and c++17 will attempt to call a copy constructor.
The key takeaway from all of this is that std::move
is a cast to an rvalue reference. It does no moving directly. Being an rvalue reference only matters to overload resolution.
The most common overload resolution it matters to is the move constructor. The compiler will write move and copy constructors for you if you don't (moving and copying each member). For base types, move and copy does the same thing; but once you write a resource management type, you can often make move insanely cheaper, and sometimes you even just block copy.
std::vector
for example has a move constructor that moves its heap allocated buffer without doing a new allocation and is O(1); meanwhile the copy constructor does a fresh heap allocation and copies each element of the vector.
So even if you don't write a move constructor, if you write a class containing a std::vector
the compiler written move constructor for your class may be insanely faster than the compiler written copy constructor. By using std::move
to indicate that an instance of your type (or something containing your type) can be moved-from safely you can get significant performance boosts. (There are even some changes in the meaning of code; a moved-from std vector has its iterators transferred implicitly)