rust-lang / nomicon

The Dark Arts of Advanced and Unsafe Rust Programming
https://doc.rust-lang.org/nomicon/
Apache License 2.0
1.75k stars 256 forks source link

Vec from scratch chapter: Why is it necessary do the for-loop on dropping? #393

Closed lauti7 closed 1 year ago

lauti7 commented 1 year ago

In chapter 9.7 on the Nomicon, when the trait Drop is implemented a for-loop on &mut self is executed with a comment saying // drop any remaining elements. Why is this for-loop needed? Because after this for-loop, the buffer itself is deallocated. What is the difference between both? I thought that it was enough with the alloc::dealloc function.

Hope you can help with this doubt! Thx in advance.

The code below:

impl<T> Drop for IntoIter<T> {
    fn drop(&mut self) {
        if self.cap != 0 {
            // drop any remaining elements
            for _ in &mut *self {}
            let layout = Layout::array::<T>(self.cap).unwrap();
            unsafe {
                alloc::dealloc(self.buf.as_ptr() as *mut u8, layout);
            }
        }
    }
}
Rianico commented 1 year ago

For a Vec, We should not only dealloc the buffer, but also execute the drop function of each element. This is just because each element may frees other resource(e.g. memory, socket, Rc) in their drop function.

lauti7 commented 1 year ago

@Rianico Thx for your quick response! Okay but the dealloc doesn't work as a drop for each element? Or the drop function is not executed when you dealloc the buffer?

Rianico commented 1 year ago

Or the drop function is not executed when you dealloc the buffer.

Yes, dealloc only free the memory, but won't execute the drop function.
For example:

use std::alloc::{self, Layout};

struct MyStruct {
    resource: u8,
}

impl Drop for MyStruct {
    fn drop(&mut self) {
        println!("free MyStruct's other resource: {}", self.resource);
    }
}

fn main() {
    let element1 = MyStruct { resource: 1 };
    let buffer = Box::into_raw(Box::new(element1));
    unsafe {
        alloc::dealloc(buffer as *mut u8, Layout::new::<MyStruct>());
    }

    let _element2 = MyStruct { resource: 2 };
}

which will just print as below:

free MyStruct's other resource: 2
lauti7 commented 1 year ago

@Rianico Ok, perfect, got it! One doubt that I have now is when a custom struct like your MyStruct is dropped, Is the memory automatically freed (dealloc) or how is this handled in the compiler?

Thx for your help!

Rianico commented 1 year ago

MyStruct is a on-stack value, it's dropped as the stack frame popped. We don't need to handle it manually. And for Vec, it allocates a sequence of memory on the heap, so we need to dealloc it in the drop function.

But that's not the point, the more imporant point can be found in 6. Ownership Based Resource Management

However we are not limited to just memory. Pretty much every other system resource like a thread, file, or socket is exposed through this kind of API.

So why we need to execute drop function for each element? Beacuse drop function of each element may free their own resources:

let ele1 = Vec[fd1, fd2];
let ele2 = Vec[fd3, fd4];
let v = Vec[ele1, ele2];

If we only dealloc v's buffer, the [fd1, fd2] and [fd3, fd4] won't be freed.

In the most case, Rust will recursively try to drop all of the fields of a struct. It's generated by the the compiler, which is super convenient, right? You can recap the 6.2. Destructors to get more details.

But in Vec, we use the raw pointer, and the compiler don't know how to free it. So we need to implement drop function to free its memory manually(dealloc), and other resource(call the drop of each element).

lauti7 commented 1 year ago

@Rianico Amazing, so good explanation! I'll see those links. Thx for your help