Why the return value was not dropped

#[derive(Debug)]

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn create_object(name: &str) -> CustomSmartPointer {
    let a = CustomSmartPointer {data: String::from(name)};
    let m = &a as *const CustomSmartPointer;
    println!("inner create_object {:?}", m);
    a
}

fn main() {
    let new_name = create_object("lolololo");
    let m = &new_name as *const CustomSmartPointer;
    println!("outside create_object {:?}", m);
    println!("{:?}", new_name);
}

output:

inner create_object 0x579eaff730
outside create_object 0x579eaff828
CustomSmartPointer { data: "lolololo" }
Dropping CustomSmartPointer with data `lolololo`!

the address of objects in function main and create_object are not the same. so, I think it creates a CustomSmartPointer object in create_object function and in main function, it copes one。But why the object in 0x579eaff730 not been dropped?

Returning the object moves it. Moving an object can change its address, but does not destroy it.

5 Likes

The question is why you expect the identical
objects to be dropped twice? Since they both contain the same pointer to the same heap-allocated buffer, that would be a double-free error.

What do you mean by "identical objects" here?

The two instances of CustomSmartPointer inside and outside the create_object() function. You create one object. You then return it which potentially memcpy()'s it into the stack frame of the caller, however its contents stay the same even at its new place. Hence, the copy is identical to the one you constructed.

(And this copy is probably elided completely when optimizations are turned on, so there'll be only one object, however that wouldn't help you understand this question, so I only mention it as an aside. Conceptually, you can always think of as moves "as-if" a bitwise copy happened.)

Does moving an object always changes its address? According to my understanding,move is different from copy in rust, moving only copies the metadata of the object on heap, Why does it need to change the address?

There are two object address, how can i tell they are the same object and contain the same pointer to the same heap-allocated? you mean, it created the object a in function create_object and allocated it in heap which address is 0x579eaff730.Then function create-object returned and assigned it to new_name in function main, and move the same object from 0x579eaff730 to 0x579eaff828 ?

You know that because one object was created from the other by returning the first which moves it (as someone already mentioned it in this thread), which is either a simple memcpy() or a complete no-op in Rust.

The addresses you are printing are NOT the pointers to the heap. By default, in the simple cases such as your example, Rust does no automatic heap allocation, you have to ask for it by using the appropriate containers. Therefore, the pointers you are printing are pointing to the stack.

However, your data structure contains a String, which performs heap allocation. So both objects at both addresses contain a String, which in turn contains a pointer to a heap-allocated buffer that is managed by the Drop impl of the String.1 When the String is moved (memcpy()'d), then this heap-allocated pointer is copied verbatim to the new place of the String. Therefore, if both strings were dropped, it would result in a double-free for the buffer on the heap.

Rust knows that Drop impls are used for managing resources, for example, freeing memory or closing file handles. Therefore, for any type that implements Drop (or transitively contains such a type), it won't call Drop::drop() on moved values, i.e. previously-allocated places of an object. An object will only be dropped from its last-used, currently owning place.


1More precisely, a String wraps a Vec and that Vec in turn wraps a RawVec, so it's not directly String::drop() that performs the deallocation, but you don't need to know this detail in order to understand the rest.

1 Like

Thanks for your explaination
So, these two pointers are still pointing to the stack? then, how can I obtain the heap address?

fn main() {
    let a = String::from("hello");
    let x = &a as *const String;
    println!("{:?}", x);
    let b = a;
    let y = &b as *const String;
    println!("{:?}", y);
}

Again, there is no heap allocation involved by default if you don't use a container type that would do it. If you write, for example:

struct MyStruct {
    field: u32,
}

let x = MyStruct { field: 42 };
let y = x;

then absolutely no heap allocation will be done. There isn't any metadata business going on. The x value of type MyStruct lives on the stack, its one field inside it lives on the stack, and when you move it by saying let y = x, then y will also live on the stack.

The difference between "moving" and "copying" in Rust is this. Types that do not "manage resources" (i.e. do not transitively need to explicitly drop()) are allowed to be marked as copiable by implementing the Copy trait. This is only a "marker" trait: it doesn't affect how the object will be dealt with by the compiler when generating code – both moving and "copying" are either a memcpy or a no-op. Stating that a type should be Copy will merely allow you to use values of that type after they have been copied bit-by-bit.

1 Like

You need to ask the String directly for its heap pointer, for instance by calling its as_ptr() method.

Again, a String is just a handle to a heap-allocated buffer. It is itself a value, so it has an address, but it's just the address of the small "handle" value that is the triple (buffer_pointer, length, capacity). If you declare such a handle with a simple let binding, those three values will live on the stack. Moving the handle around doesn't, and shouldn't, move the pointed heap buffer around.

That would have two disadvantages. First, it would be superfluous: copying over a whole long string just in order to change the address of the buffer. That would pretty much defeat an important purpose of pointers. The second would be complexity: if you ever needed to move a handle around, now you would need to make sure that its buffers are copied and adjusted and freed correctly upon moving. This would require something like move constructors in C++, supporting which would make life so much harder for users as well as compiler implementors.


Note that it is possible to create a String where the small "handle" itself also lives on the heap. For that, you'd have to explicitly heap-allocate the String object itself, for example by using Box<String> or Vec<String>.

I always thought that if a type has a moving semantics(struct by default), then it would be allocated on heap...
So, actually,a value will be still allocated on stack though it has a moving semantics as long as you don't use a container type the hold it,is that right?

No. That might be true in languages where (almost) "everything is an object", for instance Java or JavaScript or Python. In Rust, however, it is not true. In this regard, Rust is closer to C and C++, even though it abstracts most of these details away using a much stronger type system.

That is right. If you write let variable = …expression…, the value of the evaluated …expression… will be stored in variable on the stack. There will only be any heap allocation going on if the …expression… itself does that in some way, for example if it is creating a new container somewhere.


Moving and heap-allocated containers have something to do with each other, but it is not as simple as "if you move stuff around, it gets put on the heap". This might also be true in garbage-collected languages, for example, however what we are discussing here is nothing like that.

Instead, the relation is that when you have a type that needs custom code to be executed upon dropping (i.e. implements Drop or contains a Drop type), then it can't be Copy. And vice versa, types that don't (transitively) require dropping can be (although are not automatically marked as) Copy.

5 Likes

Thanks so much for your patiently explaination , I almost understand.

So for the values stored in the stack, the concepts of move and copy are identical except for the ownership semantics. In case of copy the original variable loses the ownership of the value and hence is unable to "drop" the value while in case of copy both the variables retain the ownership of their respective values (there are 2 values because of memcpy) and they drop their owned values. Is that right?

And in case the value in stack holds a pointer to the heap, it's usually move instead of copy as both the values point to the same value in heap and both variables having ownership means that it leads to the problem of double free during drop. Is this right?

1 Like

Yes. Quoting Rust a unique perspective by Matt Brubeck:

1 Like

Thanks

In case of copy both the variables retain the ownership of their respective values (there are 2 values because of memcpy) and they drop their owned values. Is that right?

No, this is incorrect. Anything implementing Drop can't implement Copy, thus can't be copied. So in case of copy, there is no dropping.

So what happens when variables, that have values that implement copy trait, go out of scope?

Nothing happens.

1 Like