Why can't I move fields out of a `Drop` type upon dropping it?

Attempting to take ownership of the fields of a dropped value which implements Drop results in the compiler error:

error[E0509]: cannot move out of type `...`, which implements the `Drop` trait

Logically, I don't understand why this is disallowed. The Drop trait takes a mutable reference to the type, meaning that at the end of the drop function, all of the dropped type's field must still be in a valid state (barring unsafe/uninit shenanigans). So why can I not then use these fields?

Minimal compiler error example (playground):

struct Foo {
    bar: String,
}

impl Drop for Foo {
    fn drop(&mut self) {}
}

fn main() {
    let foo = Foo { bar: "bar!".to_owned() };
    let Foo { bar } = foo;
    dbg!(bar);
}

For the situation that brought this to my attention, I would like to actually drop the type and invoke its Drop implementation - but then reuse some non-Copy fields within it.

You can use them no problem. What you can't do is take them out of the struct, because all fields in the struct must be valid when the destructor runs. For example, if you only borrow bar it's all fine:

fn main() {
    let foo = Foo { bar: "bar!".to_owned() };
    let Foo { bar } = &foo;
    dbg!(bar);
}

Sorry, I wasn't clear before; I've updated the topic a bit.

I would like to use (own) the fields after the drop-function has run.

Hm, interesting. I don't think it is possible. Another option would be to use std::mem::take to take ownership of the string, replacing it with an empty string.

1 Like

Maybe it would be helpful to see something closer to my real problem. I have a type BucketHandle which has mutable references to a linked list and some backing storage. When BucketHandle goes out of scope, it checks whether a node meets a certain condition, and removes it from the list if so:

struct BucketHandle<'a> {
    bkt_stg: &'a mut ll::Storage<BucketNode>,
    bkts_list: &'a mut LinkedList<BucketNode>,
    bkt_ix: usize,
}

impl<'a> Drop for BucketHandle<'a> {
    fn drop(&mut self) {
        // ...
    }
}

There is a function adjacent_bucket which consumes the input BucketHandle and returns a new one. The new handle should have references to the same linked list and backing storage.

fn adjacent_bucket<'a>(bkt: BucketHandle<'a>) -> BucketHandle<'a> {
    // `Drop` behaviour is performed here... would like to retain the mutable references
    let BucketHandle {
        bkt_stg,
        bkt_ix,
        bkts_list,
    } = bkt; // compile error
    let next_index = ...;
    BucketHandle {
        bkt_stg,
        bkts_list,
        bkt_ix: next_index,
    }
}

The only reason I can think that this is disallowed is that it's considered confusing, because the destructured values could change in the destructor; e.g. foo.bar might be a different value to &foo.bar, because a destructor has modified the value of bar in the first instance. But I can't think of any borrow-check rules that this would actually break.

To be clear, if the destructor was an ordinary function call, the borrow checker would have no issue with it. It's just that it considers the destructor to destroy the value. One thing you could do was define two layers of structs and have the destructor on the outer struct. Then you can define a method that unwraps it without running the destructor. You can do that either by putting in some other dummy value, using an Option, or by unsafe code.

2 Likes

I've found a suitable workaround for my situation: the destructor behaviour is moved to another function, and I call that instead. Then modify it as required, instead of creating a second instance of the type to return:

impl<'a> BucketHandle<'a> {
    fn perform_drop(&mut self) {
        // ...
    }
}

impl<'a> Drop for BucketHandle<'a> {
    fn drop(&mut self) {
        self.perform_drop();
    }
}

fn adjacent_bucket<'a>(mut bkt: BucketHandle<'a>) -> BucketHandle<'a> {
    bkt.perform_drop();
    let next_index = ...;
    bkt.bkt_ix = next_index;
    bkt // This behaves like a new instance
}

This is a little less neat but works. I suppose it would still be an issue BucketHandler was from another crate.

1 Like