Ok, this point needs a bit of clarification: there are three kinds of memory ressources within a program:
global / static memory, which is never freed;
global dynamically allocated and freed memory, a.k.a. the heap;
local dynamically allocated and freed memory, a.k.a. the stack;
A local in Rust thus uses the stack as its backing memory (or a CPU register as an optimization, but let's not consider that here).
When a local exits a scope / frame, it is "auto-freed" by the stack-frame shenanigans of the runtime. In the Rust model, which can be seen when looking at the Middle Intermediate Representation (MIR) of the language, allocating and deallocating these locals is respectively called
So when talking about
memcpy, the thing is that a new local is taking a value, i.e., the local is
StorageLive-stack-allocated, it is then initialized with a value / a value is written to it (and if that value comes from another part of memory, such as another variable, then that's when a (non-overlapping)
Depending on whether the initial value was
Copy or not, the original value may or may not get invalidated / Rust may forbid us from ever using that other value. One way of using a value is calling its destructor, so, when invalidated, a value will no longer run its destructor; and that's precisely the reason a type with
Drop glue / with a destructor cannot be
Copy: if it were, when each local left the scope, there would be multiple calls to the same destructor with the same data, which in the case of a destructor freeing heap-memory would lead to a double-free.
Now, a value (such as the one stored in a local variable) may be responsible of some dynamic resource, such as heap-memory, a network connection, a file handle, etc.
In that case, for the sake of convenience, of ergonomics (and thus of safety!), the resource is freed exactly right before that value ceases to exist. That's why we say that such value owns a resource, since without the owner the resource becomes unaccessible. So, something that owns something else has what is called a destructor, or drop glue in Rust parlance: a special method called when the value ceases to exist, charged to close / free / release the resources transitively owned by the value.
Box<i32>is a pointer to a heap-allocated
i32, and it owns such allocation, meaning that when a
Box<i32>is about to die, it frees the
i32that had been heap-allocated, so that such (remember global) memory may be reused elsewhere.
&'_ i32is a pointer to a
i32that does not own the memory it points to; it just knows that it can dereference-read that
i32, so as long as such thing is not done outside the range of validity of the reference, also called the lifetime
'_of the reference.
So, in the following program:
let p: Box<i32> = Box::new(42); let at_ft: &i32 = &*p;
we can represent the memory layout as follows:
at_ft are identical at runtime, they are just a number: the (memory) "address" of the heap-allocated
But, at compile-time, i.e., when analyzed by Rust,
at_ft have quite different semantics:
at_ft, for instance, borrows
p, so if (the local)
p is moved or destroyed, then the
42 in the heap may cease to exist, so Rust then forbids
at_ft from accessing the memory it points to. But other than that,
at_ft has no interaction with the
42 in the heap, so multiple
at_fts may exist without any issue whatsoever (
&i32 : Copy):
let p = Box::new(42); let at_ft = &*p; let at_ft2 = at_ft; assert_eq!(*at_ft, 42); // Fine assert_eq!(*at_ft2, 42); // Fine
This is fine because even if there are multiple pointers to the heap-allocated
42, there is only one "dotted line" (which represents ownership and thus a potential drop) to it.
The three stack-allocated addresses (i.e., the number
0x55c9b3701a40 present three times in the stack), will all be stack-deallocated when the scope they were declared in ends. That's where you were right.
However, since the heap-allocated
42 must only be deallocated at most once (it is "fine" (memory-safe) to never deallocate it; that's called a memory leak, and the only problem of so doing is exhausting memory if such thing is allowed to happen an arbitrary number of times (e.g., within a loop); small-sized memory-leaks that happen a fixed and small number of times are fine, memory-wise), what is not acceptable is having multiple "dotted lines" to
42 (that would lead to a double-free, which is undefined behavior).
p: Box<i32> was
Copyable, then after a
let p2 = p; we would have:
which would be UB after both
p2 called their respective destructors.
That's when move semantics kick in: when doing
let p2 = p;, on runtime it will be equivalent to copying the pointer, but at compile-time Rust will consider that the pointer
p has been moved, i.e., it has been deactivated: it is no longer usable, and more importantly, it no longer owns the heap-allocated
Stringis like a
Vec<T>is like a
Box<[T]>, i.e., a pointer to a heap-allocated slice (sequence) of values of type
In your OP, when you were taking addresses, you were taking addresses of the locals, i.e., of the values in the stack. And as you can see in the diagram,
p2 do not necessarily occupe the same place in the stack (it is not impossible either, as an optimisation it may often be the case, but that, of course, cannot be relied upon). With the
.as_ptr() you would have got the pointers to the heap, which, like in the diagram, point to the same place in (heap) memory.