How to write a guard that allows destructuring?

Given

pub struct WriteGuard<'a, T> {
  entry: &'a mut T
}

impl<'a, T> Drop for WriteGuard<'a, T>,
{
  fn drop(&mut self) {
    self.entry.after_change()
  }
}

I'd like to write a function that extracts an immutable reference:

impl<'a, T> WriteGuard<'a, T> {
  pub fn into_ref(self) -> &T {
    ????
  }
}

I'm fine with the destructor running or not running automatically, since I can run the same operation within into_ref().

Things I tried:

  • &*ManuallyDrop::new(self).entry, returns reference to a temporary value
  • let WriteGuard { entry } = self, cannot move out of type which implements the Drop trait

Is there no way around unsafe and pointer casting in this case?

This works, though not without cost.

You'll need to return a lifetime that isn't temporary for the duration of the call:

impl<'a, T> WriteGuard<'a, T> {
  pub fn into_ref(self) -> &'a T { // 'a here

ManuallyDrop is generally a good direction when you want to prevent Drop, but the gotcha is that ManuallyDrop(self).entry goes through Deref that gives you a shorter lifetime (IIRC there was an RFC suggesting to give ManuallyDrop magic destructuring powers).

You'll need some unsafe to get a longer lifetime. Consult with cargo miri test for what is the right hack for preserving provenance. Maybe make WriteGuard #[repr(transparent)] and transmute it to the reference type without going through ManuallyDrop?

destructuring moves the value, currently it's not possible to directly destruct a type with drop glue. see also discussion in this thread:

except for certain very contrived special cases (you have some "dummy" values so you can do std::mem::replace(), e.g. an associated constant <T as SpecialTrait>::DUMMY), the workarounds that I know of all need unsafe.

for this particular example, where your WriteGuard has only a single field, you can mark it #[repr(transparent)] and directly transmutate it into the inner field.

for more complex cases, you can ptr::read() the field you are interested in, just remember to mem::forget() (or wrap in ManuallyDrop) the old value. other fields must be handled properly too.

1 Like

Thanks for the ideas guys, I could refactor it into making the entry an Option<&'a mut T>, so I could "steal" it, and then have the Drop only do useful things if it was still a Some value.

impl<'a, T> WriteGuard<'a, T> {
  pub fn commit(&mut self) {
    if let Some(ref mut entry) = self.entry { ... }
  }

  pub fn into_ref(mut self) -> &'a T {
    self.commit(); // Let's just never forget to call this though....
    self.entry.take().unwrap()
  }
}

impl<'a, T> Drop for WriteGuard<'a, T>
where
    T: Serialize + 'static,
{
    fn drop(&mut self) {
        self.commit()
    }
}