You have to distinguish between "moving out of a location" and "destroying a location". They are not the same. Destructors only run when you actually destroy a value, and moving it does not run the destructor of the old location.
In your original example, z1
and z2
are two locations in memory. First, you create a Foo
at z1
, then you print its address. Then you move the value in z1
to z2
, and you print the address of z2
. The move operation will (without optimizations) compile down to this:
- Copy the bytes in
z1
to z2
.
- Don't touch the bytes in
z1
anymore.
So, "destroying" the z1
location isn't actually an operation. It's the absence of an operation. You just stop using the data and say that, from now, those bytes are random garbage (even though they would be a valid Foo
). Maybe you use them location for a different value later, and if so, then you're just going to overwrite the random garbage with the new value, without performing any cleanup of the old value.
Now, when z2
goes out of scope, the compiler runs the destructor of the value stored there. It runs no destructor for z1
, because the compiler keeps track of move operations and knows that z1
no longer holds anything that we need to destroy.
Once the compiler has run the destructor on z2
, the compiler will consider the bytes there as random garbage too. (Even though it doesn't actually do anything to clean it up. It probably still contains whatever bytes your Foo
value corresponded to.)
Let's add a third operation to distinguish from: "deallocating memory". This is different from the two other operations. Deallocating the memory makes the memory location unusable.
Once a memory location contains random garbage, you can either reuse it for some other value, or you can deallocate it. In the case of stack variables, deallocation does not happen until you return from the function, even if you ran the destructor earlier than that.
In the case of heap variables, this is the same. Cleaning up a Box<Foo>
corresponds to the following sequence of operations:
- Run the destructor on the memory location.
- Deallocate the memory location.
Optimizations are also a thing. When compiling with optimizations, the compiler might decide to merge two memory locations. For example, both z1
and z2
might have the same address.
If you have two memory locations, but you know that they never contain an actual value at the same time. That is, whenever one contains a value, the other is random garbage. Then, you can merge them without running into conflicts.