I try to understand the semantics of dereferencing a raw pointer. Here is an example.
use std::{alloc::{alloc, dealloc, Layout}, cell::{Cell, UnsafeCell}, fmt::Debug, marker::PhantomData, mem, ops::Deref, rc::Rc};
// #[derive(Copy, Clone)]
struct N { x : i32 }
impl Drop for N {
fn drop(&mut self) {
println!("{:p} has been dropped", self);
}
}
fn main() {
let p = unsafe { alloc(Layout::new::<N>()) as *mut N };
println!("address of p : {:p} - address of the memory(p holds) : {:?}", &p, p);
let mut bp = Box::new(N { x : 32 });
println!("address of memory(bp holds) : {:p}", bp.as_ref());
unsafe { *p = *bp; } // here
println!("address of p : {:p} - address of the memory(p holds) : {:?}", &p, p);
}
Specifically, when we assign an object to *p, the memory pointed by p should be dropped. Then, the reassignment will move the value(in heap) contained by bp to p. Here is a question, what is the value of p?
I get the following output.
address of p : 0x7ffcd66f5c88 - address of the memory(p holds) : 0x561a36082b80
address of memory(bp holds) : 0x561a36082ba0
0x561a36082b80 has been dropped
address of p : 0x7ffcd66f5c88 - address of the memory(p holds) : 0x561a36082b80
To my understanding, dereferencing a Box will consume it and return its value(ownership). After the assignment, why does the value of p not change? Should it be the address that bp holds? Or, bp's value is copied into where p is located?
Why would you expect the address to change? You are not assigning pointers anywhere. You are assigning to the pointed place after dereferencing the pointer.
A pointer is just a number giving the address of a memory location. When you dereference it you are creating a reference to that memory location. The value of p remains the same in your example because you are not changing p (that is, the memory address that p holds), and indeed you can't because p is not declared mut.
What you are doing is moving the value out of the Box and into the memory pointed to by p. Doing so drops whatever was stored in that memory, and an &mut to the memory is passed in to drop(). This &mut is actually just a reference created from p so it points to the same place (and you get the same value printed). It also points to uninitialised memory, but I'm not sure if that is undefined behaviour of you don't try to do anything with it (just creating the reference may be undefined behaviour). None of this changes the memory address stored in p though, so it is still the same at the end.
Sorry for some mistakes in the example. It is just a toy example. Never mind.
I'm a little confused about the memory management here. We know that Rust's memory management is based on "ownership". In this example, bp and p hold two different memory regions. The dereference of bp will move the ownership of its value, since the structure N is non-copy.
Thus, I think that the result of moving the ownership is to assign the memory pointed by bp to p.
Do you mean that though N is non-copy, the assignment will copy the memory pointed by bp into the memory pointed by p? If so, the incoming usage of p may cause a use-after-free error.
No, because the compiler tracks moves. A place that is moved from cannot be used again, and its contents will not be dropped.
Also, you seem to be expecting a pointer to change address just because you are changing the pointed value. I'm not sure how you imagine that to work physically/mechanistically, but it looks like you have an entirely inaccurate mental model of memory addressing.
I think the misunderstanding might be lurking here. Please accept my apology in advance if this is just a terminological nitpick about something that you already understand.
Rust ownership is of values (sequences of bytes), not the memory that stores those values. p points to some memory, and it points to the same memory throughout your program. When you perform the assignment, the value stored in*p is dropped, but p is still pointing to the same memory — it's just now counted as “moved out of” (aka de-initialized; invalid to access) by the ownership rules. Then the assignment operation immediately moves a new value into that memory.
In general, allocation and deallocation are done by alloc::alloc() and alloc::dealloc(), and dropping values is done by drop_in_place() (which is usually invoked implicitly by the compiler; it is usually only done explicitly as needed by by heap-allocating types types like Box,[1]Vec and Rc.)
The important thing here is that dropping and deallocation are separate things — and allocating and moving in are separate things — and they can happen at separate times. Moving a value in or out of an allocation doesn't change what that allocation is in any way (especially not its address), only what value it holds.
Deallocation is often the next step after dropping a value, but not necessarily. In particular, in your original program, you actually have moved the N out of *bp, so you have a Box<N> that has no N in it. But that heap allocation still exists and you're allowed to assign a new N into it. This is exactly the same thing (though implemented very differently) as, say, pop()ing a value out of a Vec — the allocation is the same, but it has one fewer value in it.
Printing a raw pointer will print its address (as opposed to printing references, which will print the pointed-to value). You'll want to dereference p when printing it.
Also consider running your code under MIRI, as you're doing a bunch of potentially UB stuff here (e.g. dropping uninitialized memory when assigning *p = *bp).
I misunderstand the "move" semantic and "drop" semantic in Rust. Thus, I get an incorrect image in memory.
A value "move" just tells us that its memory(aka. the memory p points to) is copied and becomes invalid. And "drop" does not mean dealloc(maybe I should read the document carefully).
Thanks again. I get it.
One more question, does drop mean that the memory becomes invalid?
Dropping a value is a kind of moving that value. So, the memory that the value previously occupied is now in the moved-out-of state — it does not contain a valid value.
But I would not put that as “the memory is invalid”. The memory is perfectly good memory which you can continue to do other things with — you can write another value of the same type to it, or repurpose it to hold something else entirely. You just can no longer soundly read a value of type N (or whatever) out of it, because that particular value has been moved.
Thinking of this in terms of state transitions, memory has three states relevant to the ownership model:
Not part of an allocation (that you know about).
You may not do anything with this memory, but if you call alloc() the allocator might give it to you, transitioning to state 2.
Part of an allocation, but not initialized.
You may deallocate it, transitioning to state 1.
You may write a value to it, transitioning to state 3.
Contains a value of some type T.
You may read and write to it (subject to the rules of borrowing and of valid operations for the type T).
You may copy the bytes elsewhere, transitioning to state 2.
If T is Copy, you may copy the bytes elsewhere and stay in state 3.
In many cases, we use safe operations like Box::new() which go from state 1 to state 3 as a unit, and drop operations which go from state 3 to state 1, without noticing the state 2 in the middle. But state 2 still exists.
(Also, the fundamental thing about Rust ownership that is different from other languages is the rule "in state 3, you may copy the bytes elsewhere, transitioning to state 2." Other languages don't systematically enforce this state transition in a programmer-visible way; they either leave it up to you to track, or do something else entirely like refcounting/GC-based ubiquitous sharing.)
No, not really. Neither of these statements is correct as-is. Instead of thinking in terms of analogies, you should, again, consider how things are represented in memory. Fortunately, that is completely straightforward, there is no magic.
In the case of an owning smart pointer like Drop, you have to consider what happens to the pointed value as well as the pointer itself:
assigning to the referent of the Box drops the old value and overwrites it with the new value. This does NOT, of course, deallocate the heap space represented by the Box.
dropping the Box itself drops the inner value, because the Box owns it. It also necessarily deallocates the heap place, because that's also a resource the box manages.
In contrast, when you overwrite a value behind a non-owning pointer (such as a regular &mut reference), the old value still gets dropped, and the new value is still moved (bitwise copied) to the place, but assigning to the pointer itself wouldn't do any of that — since the pointer does not own the pointed place.
(Incidentally, you should be very aware at this point that indirection, heap allocation, and ownership are completely and mutually orthogonal. Neither implies the other two.)
BTW I'm not sure what extra meaning you are attributing to trivial concepts like "allocation" and "dropping", but you are likely overcomplicating this in your head, trying to read too much into what is otherwise a very simple mechanism. You should probably take a step back and consider how you think things should work if you were to design the ownership system yourself. Then you'll see everything we described here is actually completely natural and doesn't really require any complicated concepts.
The rules around dropping and assignment serve purely two purposes:
not to leak resources (prevent the error of too few drops/deallocations/resource relinquishment)
not to double-free resources (prevent errors related to deallocating/relinquishing resources too much, and in general prevent use-after-free/use of invalidated values and places).