Destructors
What the language does provide is full-blown automatic destructors through theDrop
trait, which provides the following method:
fn drop(&mut self);
This method gives the type time to somehow finish what it was doing.
After drop
is run, Rust will recursively try to drop all of the fieldsof self
.
This is a convenience feature so that you don’t have to write “destructorboilerplate” to drop children. If a struct has no special logic for beingdropped other than dropping its children, then it means Drop
doesn’t need tobe implemented at all!
There is no stable way to prevent this behavior in Rust 1.0.
Note that taking &mut self
means that even if you could suppress recursiveDrop, Rust will prevent you from e.g. moving fields out of self. For most types,this is totally fine.
For instance, a custom implementation of Box
might write Drop
like this:
#![feature(ptr_internals, allocator_api)]
use std::alloc::{Alloc, Global, GlobalAlloc, Layout};
use std::mem;
use std::ptr::{drop_in_place, NonNull, Unique};
struct Box<T>{ ptr: Unique<T> }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
drop_in_place(self.ptr.as_ptr());
let c: NonNull<T> = self.ptr.into();
Global.dealloc(c.cast(), Layout::new::<T>())
}
}
}
# fn main() {}
and this works fine because when Rust goes to drop the ptr
field it just seesa Unique that has no actual Drop
implementation. Similarly nothing canuse-after-free the ptr
because when drop exits, it becomes inaccessible.
However this wouldn’t work:
#![feature(allocator_api, ptr_internals)]
use std::alloc::{Alloc, Global, GlobalAlloc, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;
struct Box<T>{ ptr: Unique<T> }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
drop_in_place(self.ptr.as_ptr());
let c: NonNull<T> = self.ptr.into();
Global.dealloc(c.cast(), Layout::new::<T>());
}
}
}
struct SuperBox<T> { my_box: Box<T> }
impl<T> Drop for SuperBox<T> {
fn drop(&mut self) {
unsafe {
// Hyper-optimized: deallocate the box's contents for it
// without `drop`ing the contents
let c: NonNull<T> = self.my_box.ptr.into();
Global.dealloc(c.cast::<u8>(), Layout::new::<T>());
}
}
}
# fn main() {}
After we deallocate the box
‘s ptr in SuperBox’s destructor, Rust willhappily proceed to tell the box to Drop itself and everything will blow up withuse-after-frees and double-frees.
Note that the recursive drop behavior applies to all structs and enumsregardless of whether they implement Drop. Therefore something like
struct Boxy<T> {
data1: Box<T>,
data2: Box<T>,
info: u32,
}
will have its data1 and data2’s fields destructors whenever it “would” bedropped, even though it itself doesn’t implement Drop. We say that such a typeneeds Drop, even though it is not itself Drop.
Similarly,
enum Link {
Next(Box<Link>),
None,
}
will have its inner Box field dropped if and only if an instance stores theNext variant.
In general this works really nicely because you don’t need to worry aboutadding/removing drops when you refactor your data layout. Still there’scertainly many valid usecases for needing to do trickier things withdestructors.
The classic safe solution to overriding recursive drop and allowing moving outof Self during drop
is to use an Option:
#![feature(allocator_api, ptr_internals)]
use std::alloc::{Alloc, GlobalAlloc, Global, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;
struct Box<T>{ ptr: Unique<T> }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
drop_in_place(self.ptr.as_ptr());
let c: NonNull<T> = self.ptr.into();
Global.dealloc(c.cast(), Layout::new::<T>());
}
}
}
struct SuperBox<T> { my_box: Option<Box<T>> }
impl<T> Drop for SuperBox<T> {
fn drop(&mut self) {
unsafe {
// Hyper-optimized: deallocate the box's contents for it
// without `drop`ing the contents. Need to set the `box`
// field as `None` to prevent Rust from trying to Drop it.
let my_box = self.my_box.take().unwrap();
let c: NonNull<T> = my_box.ptr.into();
Global.dealloc(c.cast(), Layout::new::<T>());
mem::forget(my_box);
}
}
}
# fn main() {}
However this has fairly odd semantics: you’re saying that a field that shouldalways be Some may be None, just because that happens in the destructor. Ofcourse this conversely makes a lot of sense: you can call arbitrary methods onself during the destructor, and this should prevent you from ever doing so afterdeinitializing the field. Not that it will prevent you from producing any otherarbitrarily invalid state in there.
On balance this is an ok choice. Certainly what you should reach for by default.However, in the future we expect there to be a first-class way to announce thata field shouldn’t be automatically dropped.