I'm trying to better understand how moving works in C++. How does the compiler know when to move and when not to move?
#include <iostream>
template <typename T>
struct Container {
T value;
Container (T value): value(value) {}
Container (const Container & i): value(i.value) {
std::cout << "copying" << std::endl;
}
Container (Container && i): value(std::move(i.value)) {
std::cout << "moving" << std::endl;
}
};
void increment (Container<int> n) {
std::cout << "incremented to " << ++n.value << std::endl;
}
int main () {
Container<int> n (5);
increment(std::move(n));
std::cout << n.value << std::endl;
}
This example prints
moving
incremented to 6
5
So I'd expect the int
to have been moved, but then I shouldn't be able to still use it afterwards (and get the original value).
OK, so maybe the int
was copied because value(std::move(i.value))
copied it in the move constructor. But I still don't understand why Container<int> n
is still around after it's definitely been moved.
std::move
really just changes a value into an rvalue, which can be moved. If you apply this to an int
it has no real effect, since it doesn't make any sense to "move" an int
value. (It's true that the value is still an rvalue; it's just that having an rvalue reference to an int
is generally no more useful than having any other kind of reference to an int
).
This is because moving is meant to be about transferring resources from one object to another, in such a way that the need to duplicate those resources (by copying them) is avoided - because such duplication can be non-trivial; it might require dynamic memory allocation, for one thing. Copying an int
value is trivial, so there is no need for special move semantics.
So, applied to your example, moving the Container<int>
is exactly the same as copying it, except of course for the output ("moving" vs "copying").
(Note that even a move requires the source object to remain in a valid state after the operation completes - it doesn't destroy the source object, as you seem to think that it perhaps should).
As to how the compiler knows when it can move vs copy, this is a matter of the type category. Your use of std::move
specifically changes the value's type category to rvalue (or more specifically to xvalue) and this type of value can match the rvalue reference parameter in your move constructor. In general, overloads with rvalue reference parameters are preferred to those with non-rvalue reference parameters (the precise rules are complex).
The other common way of producing an rvalue is as an unnamed temporary - by performing some operation on an object or value where the result isn't bound to a variable (a + b
, where a
and b
are both of type int
, is a simple example - the result is a temporary object; it doesn't exist in a variable of its own). When a more complex object is a temporary, moving it into its final destination can be more efficient than copying it, and is safe since the indeterminate state of the moved-from object cannot be used afterwards. So, such values are also rvalues and will bind to rvalue references (and may be moved).
So I'd expect the int to have been moved
Primitive types like int
have no move constructor, they are simply copied.
but then I shouldn't be able to still use it afterwards
That is not how moving works. You can still use a moved-from object just fine - it will be in a valid state. However, that state is indeterminate, so you cannot expect the object to have any specific value, nor can you use operations that have preconditions. There are no operations on int
objects that have preconditions as far as I am aware.
OK, so maybe the int was copied because value(std::move(i.value)) copied it in the move constructor.
Yes, it was copied.
But I still don't understand why Container n is still around after it's definitely been moved.
Objects do not disappear after being moved from. In general, they are left in a valid, but indeterminate state. This particular move constructor that you wrote happens to leave the object in exactly the same state as it was before being moved from.
When does the compiler move ... in C++
When copy-initialization is called with a non-const rvalue argument and the type of the object has a move constructor. Or when an object is assigned with a non-const r-value operand, and the type of the object has a move assignment operator.
How does the compiler know when to move and when not to move?
The compiler knows the types of all expressions. In particular, it knows whether the type of an expression is a non-const rvalue or not. The compiler also knows definitions of all types that you can move. Based on the definition, the compiler knows whether a type has a move constructor.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With