Ben Pious
Ben Pious

Reputation: 4805

How can I move a value out of the argument to Drop::drop()?

I'm using gfx-hal, which requires me to create resources which need to be explicitly destroyed using functions specific to their type. I'd like to store instances of these types in structs, and I'd also like to tie cleaning them up to the lifetime of the owning struct, instead of managing their lifetimes manually and potentially having objects on the GPU/in the driver live forever.

However, all the functions in the destroy family of functions take the type directly, rather than a reference, so when I try to pass them from my structs, I get errors like the following:

error[E0509]: cannot move out of type `S`, which implements the `Drop` trait
 --> src/lib.rs:9:18
  |
9 |         destroyT(self.member)
  |                  ^^^^^^^^^^^ cannot move out of here

It seems like there should be some way around this issue, as I'm currently in the Drop::drop function itself, so self is already "consumed." How do I get the instances of these types out of self as T, and not &T?

struct T;

struct S {
    member: T,
}

impl Drop for S {
    fn drop(&mut self) {
        destroyT(self.member)
    }
}

// elsewhere, in a library

fn destroyT(t: T) {
    //...
}

Upvotes: 15

Views: 2642

Answers (3)

Mark Saving
Mark Saving

Reputation: 1797

You can use the consume_on_drop crate to do this. Full disclosure: I am the author of this crate.

extern crate consume_on_drop;
use consume_on_drop::{Consume, ConsumeOnDrop};
use std::ops::{Deref, DerefMut};

// We have a library given to us.
mod library_code {
    pub struct T;
    pub fn destroyT(_t: T) {
        // code ommitted
    }
    impl T {
        pub fn borrow_t(&self) {
            // code ommitted
        }
        pub fn borrow_mut_t(&mut self) {
            // code ommitted
        }
    }
}

use library_code::T;

// We can't implement consume for T itself, since T is defined
// in a library. We need a wrapper that implements Consume.
struct TConsume(T);

// The Consume trait defines what happens to our TConsume when
// we drop a value of type ConsumeOnDrop<TConsume>.
impl Consume for TConsume {
    fn consume(self) {
        library_code::destroyT(self.0)
    }
}

// Dropping S will call destroyT on the underlying T.
// S is a zero-overhead wrapper around T.
pub struct S(ConsumeOnDrop<TConsume>);

impl Deref for S {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0.0
    }
    // We can now call s.borrow_t() where s: S
}

impl DerefMut for S {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.0.0
    }
    // We can now call s.borrow_mut_t() where s: S
}

impl S {
    /// Turn our S back into the underlying T
    pub fn into_inner(self) -> T {
        ConsumeOnDrop::into_inner(self.0).0
    }

    /// Turn a T into an S
    pub fn new(val: T) -> Self {
        Self(ConsumeOnDrop::new(TConsume(val)))
    }
}

fn main() {
    let mut s = S::new(T);
    s.borrow_t();
    s.borrow_mut_t();
    drop(s); // Calls destroyT on underlying T
}

Alternately, if you don't care about giving the type S a name, you can use WithConsumer. In the code below, s is a zero-overhead wrapper around T which, when dropped, will have the consumer library_code::destroyT called on its underlying T.

You can also use a closure as a consumer, though closures may take up a nonzero amount of space (unlike a statically known function item like library_code::destroyT). This approach has the benefit of brevity, but you cannot actually name the type of s. It's possible that in the future, support for impl Trait will improve to the point that we can always use WithConsumer over ConsumeOnDrop.

extern crate consume_on_drop;
use consume_on_drop::WithConsumer;

mod library_code {
   ... // same library code as above
}

fn main() {
    let mut s = WithConsumer::new(T, library_code::destroyT);
    s.borrow_t();
    s.borrow_mut_t();
    drop(s); // calls destroyT on the underlying T
}

Upvotes: 0

kmdreko
kmdreko

Reputation: 60457

You can use ManuallyDrop. It has no overhead and a clear API. It is less of a pain to use than MaybeUninit since it doesn't require unsafe to use elsewhere (access to the inner value is available by simple Deref) and it only requires unsafe to actually move the value out.

use std::mem::ManuallyDrop;

struct T;

impl Drop for T {
    fn drop(&mut self) {
        println!("dropping T");
    }
}

struct S {
    member: ManuallyDrop<T>,
}

impl Drop for S {
    fn drop(&mut self) {
        // SAFETY: this is safe because we do not access `self.member` any more
        let member = unsafe { ManuallyDrop::take(&mut self.member) };
        destroy_t(member);
        
        // dropping a `ManuallyDrop` does nothing
    }
}

fn destroy_t(_t: T) {
    println!("destroy T");
}

fn main() {
    let _x = S {
        member: ManuallyDrop::new(T),
    };
}
destroy T
dropping T

Keep in mind, as evidenced by the name, ManuallyDrop will prevent the inner value from being dropped unless ManuallyDrop::take or ManuallyDrop::into_inner is called (thereby removing it from protections and normal drop rules will apply) or if ManuallyDrop::drop is used.

Upvotes: 6

Shepmaster
Shepmaster

Reputation: 431479

The safest, easiest way to do this is to use an Option:

struct T;

impl Drop for T {
    fn drop(&mut self) {
        println!("dropping T");
    }
}

struct S {
    member: Option<T>,
}

impl Drop for S {
    fn drop(&mut self) {
        if let Some(t) = self.member.take() {
            destroy_t(t);
        }
    }
}

fn destroy_t(_t: T) {
    println!("destroy T");
}

fn main() {
    let _x = S { member: Some(T) };
}

You could choose to use unsafe code with MaybeUninit and swap out the current value for an uninitialized one:

use std::mem::{self, MaybeUninit};

struct T;

impl Drop for T {
    fn drop(&mut self) {
        println!("dropping T");
    }
}

struct S {
    member: MaybeUninit<T>,
}

impl Drop for S {
    fn drop(&mut self) {
        let invalid_t = MaybeUninit::uninit();
        let valid_t = mem::replace(&mut self.member, invalid_t);
        let valid_t = unsafe { valid_t.assume_init() };
        destroy_t(valid_t);
        // Dropping MaybeUninit does nothing
    }
}

fn destroy_t(_t: T) {
    println!("destroy T");
}

fn main() {
    let _x = S {
        member: MaybeUninit::new(T),
    };
}

See also:

Upvotes: 11

Related Questions