Drop Check Drop Trait Implementation

I've read through the drop check explanation in this post: Drop Check - The Rustonomicon.

I understood that fields of structs and tuples are dropped in the order of their definition.

So why, in this code example, does the inspector get dropped first, followed by days, but the borrow checker still throws the error "**borrow might be used here, when world is dropped and runs the destructor for type World<'_>**"?

struct Inspector<'a>(&'a u8);

impl<'a> Drop for Inspector<'a> {
   fn drop(&mut self) {
       println!("I was only {} days from retirement!", self.0);
   }
}

struct World<'a> {
   inspector: Option<Inspector<'a>>,
   days: Box<u8>,
}

fn main() {
   let mut world = World {
       inspector: None,
       days: Box::new(1),
   };
   world.inspector = Some(Inspector(&world.days));
   // Let's say `days` happens to get dropped first.
   // Then when Inspector is dropped, it will try to read free'd memory!
}

=> So adding the implementation for Drop trait will causes the compiler more strict ?

You're creating a self-referential struct, which besides generally being unusable, is incompatible with having a non-trivial destructor.

The order of fields doesn't matter. You have to exclusively borrow the struct for the rest of it's validity ("forever") in order to create the self-referential struct. That is incompatible with having a non-trivial destructor, as that is also an exclusive action.

2 Likes

I just wonder why adding the Drop trait implementation would break the code. Still confused.

Adding a Drop implementation is one way to have a non-trivial destructor. And having a non-trivial destructor does create more borrow-check constraints than having a trivial destructor, because a non-trivial destructor might observe or even mutate the object being destructed.

In contrast, the compiler knows that trivial destructors don't observe the object being destructed.


From a high-level view, borrow checking works by analyzing

  • First, when places (such as variables) are borrowed, and how (e.g. a shared borrowe or exclusive)
  • The, whether each use of a place conflicts with the borrows active at that use site

Having a non-trivial destructor is a different kind of use than a trivial (or non-existent) destructor.


Consider this example. s drops before m, which means that when m drops, the String no longer exists. If you run the playground in the link, it will compile anyway -- because the compiler recognizes that MaybeLooksAt has a trivial destructor that doesn't actually try to read the already-freed String.

But if you uncomment the Drop implementation, then MaybeLooksAt has a non-trivial destructor which requires exclusive access (Drop::drop takes a &mut self) and might observe all reachable data (for example the destructor in the playground prints the &str). So the playground can -- correctly! -- no longer compile. If it compiled, there would be a use-after-free.

4 Likes