How to forget one type, but drop a child field

I have a type (let's call it A) with a Drop implementation that could potentially panic (in this case, because a mutex was poisoned), and in turn contains another type with a drop implementation (specifically, an Arc).

I'd like to add an API that allows the user to consume the A, but instead of panicking, returns a Result that has an error in the error condition. The problem is that if the drop method is called after getting the result, then drop will panic, so I need to forget the A, which is fine, but I also want to drop the Arc contained in it.

I can do this with unsafe code like:

pub fn consume_result(mut self) -> Result<(), Error> {
    let res = self.inner.get_result();
    unsafe { std::ptr::drop_in_place(&mut self.inner) };
    std::mem::forget(self);
    res
}

But I'd like to know if there is a way to do this without unsafe code. In an ideal world, the compiler would be able to figure out that I am about to forget self, so it is safe to move a value out of it (unless there is some safety problem with that, that I'm not aware of).

Is it possible to break the structure into several parts (consuming it but not dropping the contents), drop the part containing Arc and forget the part containing Mutex? Like this (simplified, may not be applicable directly):

struct Combined {
    mutex: Mutex<()>,
    inner: Arc<()>,
}

impl Combined {
    fn consume(self) {
        let Self { mutex, inner } = self;
        mem::forget(mutex);
        // `inner` is implicitly dropped
    }
}
2 Likes

This is a red flag for me. Code that gets run inside a Drop implementation should never panic because you run the risk of triggering a double-panic and aborting.

You could probably avoid a lot of this awkwardness by making sure Drop just doesn't panic.

Can you show an example definition for this type?

My gut reaction is to insert an Option and then do self.field.take().expect("Unreachable: can't be consumed twice") in the consume() method. That way when the struct is dropped, our field won't contain anything so there won't be any issues.

Would something like that work?

Otherwise, you can use a ManuallyDrop if you want more fine-grained control over if/when a field gets destroyed.

2 Likes

let Self { mutex, inner } = self doesn't work. I get the error: "cannot move out of type Combined which implements the Drop trait." See Rust Playground

1 Like

Can you show an example definition for this type?

use std::sync::{Mutex, Arc};
use std::mem::ManuallyDrop;

pub struct Test {
    inner: ManuallyDrop<Arc<Mutex<u32>>>,
}

impl Test {
    pub fn consume(mut self) -> Result<(), ()> {
        let res = self.mark_done();
        unsafe { ManuallyDrop::drop(&mut self.inner) };
        res
        // `inner` is implicitly dropped
    }
    
    fn mark_done(&mut self) -> Result<(), ()> {
       if let Ok(mut lock) = self.inner.lock() {
           *lock = 0;
           Ok(())
       } else {
           Err(())
       }
    }
}

impl Drop for Test {
    fn drop(&mut self) {
        self.mark_done().unwrap();
        unsafe { ManuallyDrop::drop(&mut self.inner) };
    }
}

My gut reaction is to insert an Option and then do self.field.take().expect("Unreachable: can't be consumed twice") in the consume() method. That way when the struct is dropped, our field won't contain anything so there won't be any issues.

I thought about that, but it complicates the code, because now everything has to worry about that field being an option, even though it will never be None, except right before the object is forgotten.

Generally the answer these days is that you don't want to use forget, but rather ManuallyDrop -- forget is implemented in terms of ManuallyDrop.

So you could try something like

let this = ManuallyDrop::new(self);
unsafe { ptr::drop_in_place(&mut this.inner) };

using drop_in_place in std::ptr - Rust, which is basically the not-safe version of what Michael-F-Bryan said earlier.

1 Like