Impl DropOwned?

Just randomly curious: Was there ever a time where it was considered if Drop should take ownership instead of &mut self, or if there should be an alternative DropOwned which does that? (With the obvious compiler support to not make it recurse into itself).

(Asking because of partial moves)

1 Like

I suggest you repost this on https://internals.rust-lang.org/ since that's where the design of the language (present, future, past, and hypothetical) is discussed.

IIRC it's been considered, but it creates a situation where you can make self escape drop() (save in a global variable), and end up with an object that stays alive after Drop, and will have Drop called on it again elsewhere later. it's solvable, but a weird case to deal with.

There's ManuallyDrop::take() for moving fields in drop.

2 Likes

Yeah, I work around the partial move issues with a combination of Option::take() and ManuallyDrop::take(), but the issue with ManuallyDrop is that I know that one of these days I will forget to drop something (.. if I haven't already!).

What about something like this ?

trait DropOwned {
    fn drop(self);
}

struct Owned<T: DropOwned>(ManuallyDrop<T>);

impl<T: DropOwned> Drop for Owned<T> {
    fn drop(&mut self) {
        let this = unsafe { ManuallyDrop::take(&mut self.0) };
        DropOwned::drop(this);
    }
}

1 Like

One version of this that would've avoided the double-drop issue is forcing you (via compiler magic) to use destructuring in the receiver. Drop has some magic to it anyway (preventing calling from own code), so this wouldn't be a "worse" solution in that sense.

struct MyStruct{
    a: String,
    b: Vec<i32>
}

impl Drop for MyStruct {
    fn drop(MyStruct{a, b}: Self) {
        println!("{a}");
        consume_vec(b);
    }
}

fn consume_vec(_: Vec<i32>){ unimplemented!() }

This would break the way pinning works today, but pinning was added later to the language. Does anyone know why this design wasn't discussed?

Looks like it was.

1 Like

I think if it were possible at all to destructure in that way (currently you cannot move out of a struct that has a Drop impl), it'd make sense to make it possible to do more places than just Drop:

let bypass_drop!(MyStruct {a, b}) = my_struct;

The main use case for this outside of Drop would be to “disarm” guard objects, causing something to be done that is different than, and mutually exclusive with, if they were just dropped. Right now, to implement that you have to, depending on the situation, do one or both of:

  • Use Option in the type of each field that should be moved out of
  • Call std::mem::forget() on the struct afterward so its Drop doesn't run

This destructuring operation would remove the need for both.

2 Likes

Implementing such a bypass_drop! macro is actually already kind of possible [1]:

macro_rules! bypass_drop{
    (let $struct_name: ident { $( $field: ident $(: $rename: pat )? ),* $(,)? } = $value: expr ; ) => {
        fn __check_duplicates_and_exhaustiveness() {
            #[allow(unreachable_code)]
            let _ = $struct_name {
                $(
                    $field: unreachable!()
                ),*
            };
        }
        let __manually_drop = ManuallyDrop::new($value);
        $(
            //SAFETY: the trickery above ensures that no fields are moved out of twice
            //SAFETY: the struct is in a ManuallyDrop, so the destructor can't observe the value
            __unsafe_field!(__manually_drop $field $($rename)?);
        )*
    };
    (let $struct_name: ident { $( $field: ident $(: $rename: pat )? , )* .. $(,)? } = $value: expr ; ) => {
        #[allow(unreachable_code)]
        let __value = match true {
            false => {
                $struct_name {
                    $(
                        $field: unreachable!(),
                    )*
                    ..unreachable!()
                }
            },
            true => $value
        };
        
        let __manually_drop = ManuallyDrop::new(__value);
        $(
            //SAFETY: the trickery above ensures that no fields are moved out of twice
            //SAFETY: the struct is in a ManuallyDrop, so the destructor can't observe the value
            __unsafe_field!(__manually_drop $field $($rename)?);
        )*
    };
}

macro_rules! __unsafe_field{
    ($manually_drop: ident $field: ident $rename: pat) => {
        let $rename = unsafe{ ptr::read(& $manually_drop.$field) };
    };
    ($manually_drop: ident $field: ident) => {
        let $field = unsafe{ ptr::read(& $manually_drop.$field) };
    };
}

Playground

Although I wasn't able to make it work for tuple structs.


  1. whether ptr::reading the fields out is unsound or not is up for debate, I think it should be sound and Miri doesn't flag it either ↩︎

1 Like

I also found a crate owned-drop that implements this idea.

1 Like