How Drop clear memory

I've learned from the book that Drop trait is used for clearing an object, but still very confused about this.

First, Is the Drop method just a function that will be called before rust really free the object internally? So the real code may look like:

// when obj goes out of scope, run
obj.drop();
unsafe_deallocate(obj);

If not so, that means Drop function takes the responsibility to free the memory itself, but I didn't see anything about this in rust book's sample code:

struct HasDrop;

impl Drop for HasDrop {
    fn drop(&mut self) {
        println!("Dropping!");
    }
}

fn main() {
    let x = HasDrop;

    // Do stuff.

} // `x` goes out of scope here.

From the code above, Drop just prints a string, and doesn't call functions like free in C language or anything like deallocate(self), so how is this?

If I have a struct like:

struct Foo{
  val: i32,
  next: Option<Box<Foo>>
}

impl Drop for Foo [
  fn drop(&mut self) {
    println!("hello world");
  }
}

Would this cause memory leak, because it does nothing in drop?

1 Like

Drop::drop is a function called just before the value it is called on is deallocated. The only case where Drop has to deallocate anything is when you've allocated something Rust doesn't already know how to deallocate. For example, if you allocate via a C API, you need to deallocate manually.

The expectation is that types handle deallocation in Drop if necessary; users don't need to. You never explicitly deallocate a Box because you're not responsible for that, Box is.

This doesn't really happen. Stack storage is just... not used any more. If there's some kind of explicit deallocation, it happens in Drop for the type that's responsible for said allocation.

No.

Incidentally, if you're asking for help and posting example code, you should make sure it's as valid as you can before posting. You've got a [ in there, and printfln isn't a thing. Doesn't matter in this case, but I've seen threads get bogged down because the example given doesn't do/mean what the poster intended, because it wasn't checked.

3 Likes

Thx for your patient reply.
So we don't need to care about deallocating the memory ourselves in most cases, rust will do it, right?
There are only two cases we would do that our own:

  • When Rust don't know about them(allocated by C...)
  • Recursive structure like linked-list that may cause stack overflow, we also don't need to care about freeing memory, what we do is just rewriting recursive Drop to While loop Drop

Yes, just like any half-way sane language. :slight_smile:

Just for clarity: it's not that Rust can't free these structures, it's that it might run out of stack space while doing so. In those cases, it's not about manually freeing memory, it's about manually controlling the order in which the automatic deallocation happens.

3 Likes

Ok, to be clear, let's see what Rust does with the following code:

struct Foo (
    /*0:*/ i32,
    /*1:*/ Box<i32>,
);

fn foo ()
{
    let x = Foo (
        /*0:*/ 42,
        /*1:*/ Box::new(27),
    };
}

This generates (with cargo +nightly rustc -- -Zunpretty=mir-cfg and xdot -, for those curious) the following MIR:

  • As you can see, two locals were created:

    1. First _2, which is a temporary local used to store the result of Box::new(27),

    2. Then _1, which corresponds to our x variable.

  • Both are stack-allocated (StorageLive) at the beginning of the function, but _2 is disposed of / stack-freed (StorageDead) right after its contents have been "moved" into the second field of _1.

  • Then the function ends and x reaches the end of its scope:

    1. drop(_1) is automagically called (even though there was no impl Drop for Foo in our code);

      • This will, in turn, recursively drop each field of x: Foo, i.e.:

        • drop x.0: i32 (no-op),

        • drop x.1: Box<i32> (since Box<i32> does have drop glue);

        • Note: the "recursively-called" drop() order is unspecified; i.e., you cannot rely on x.0 being dropped before x.1 !

    2. x is stack-freed: StorageDead(_1)

Note how no "dropping" code was written by us, only by Rust. And the only way Rust manages to do it soundly is by requiring you to code with ownership in mind (it's only when an owner / owned value reaches the end of its scope that drop() is automagically called).

6 Likes

Might I also add, that you shouldn't confuse these two:

An example:

fn main() {
    struct Foo(pub usize);
    impl Drop for Foo {
        fn drop(&mut self) {
            println!("Hello, I'm being dropped");
        }
    }
    let x = Foo(0);
} //Look below to see what rust does!

So rust does this (Naively, it's a bit more complicated because 0 is 'static and can be safely reinterpreted):

+ Foo // Call it x //    let x = Foo
Move `0` into x //       let x = Foo(0);
//Scope ends here // }
+ Call <Foo as Drop>::drop(&mut x)
+     println!("Hello, I'm being dropped");
- Allocator "drop" of x

Another example is that of std::mem::drop<T>(_: T):

let x = Foo(0);
drop(x);

turns into (roughly):

//... allocate and initialize x
+ drop(x) //x is now in drop's scope, so the same thing happens as above
- // call <Foo as Drop>::drop(&mut x) and deallocate

Please note, that as mentioned in the docs for std::mem::drop(_)

This function is not magic; it is literally defined as

pub fn drop<T>(_x: T) { }
1 Like