`impl Drop` vs destructors vs memory cleanup

Correct my understanding. In the following:

use std::mem::ManuallyDrop;

fn main() {
    let test = Test::new(1);
    let test2 = ManuallyDrop::new(Test::new(2));
    let test3 = Box::new(Test::new(3));
    let test4 = ManuallyDrop::new(Box::new(Test::new(4)));
}

#[derive(Debug)]
struct Test(i32);

impl Test {
    fn new(n: i32) -> Self {
        let test = Test(n);
        println!("new `{:?}`", test);
        test
    }
}

impl Drop for Test {
    fn drop(&mut self) { 
        println!("dropping `{:?}`", self);
    }
}

despite the output being

new `Test(1)`
new `Test(2)`
new `Test(3)`
new `Test(4)`
dropping `Test(3)`
dropping `Test(1)`

the only proper memory leak is with the Test(4) as, unlike the rest, it was explicitly alloc-ated on the heap viaBox::new. Given that the inner dealloc call was in <Box as Drop>::drop(&mut self), which was never called due to ManuallyDrop, that memory will remain unclaimed until 'static.

Regardless of whether the impl Drop (the destructor?) is called, all memory on the stack within any given scope is automatically "reclaimed" at the end of that scope. The same isn't true for heap allocations which, by their very nature, require either explicit or implicit (new/drop) calls.

Am I missing anything here?

1 Like

The space on the stack is always reclaimed when a function returns, so there's no way to leak the stack memory itself.

You still need to be careful about data on the stack that owns data allocated elsewhere, e.g. File itself can be placed on the stack (the file descriptor is just an integer, and doesn't need to be heap-allocated), but if you leak the File, the file descriptor won't get closed, and other memory associated with it won't be released.

3 Likes

To be precise, a type’s destructor consists of its Drop::drop() implementation, if any, followed by the destructors of each of its fields.

1 Like