10

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) {
    //...
}
Shepmaster
  • 326,504
  • 69
  • 892
  • 1,159
Ben Pious
  • 4,675
  • 2
  • 19
  • 32
  • 2
    Looks like you're not the only one to find this frustrating: https://github.com/gfx-rs/gfx/issues/2452 – loganfsmyth Nov 12 '18 at 02:07
  • Couldn't you use a NewType for `T` that implements `Drop` and calls `destroy()`. That way, the `Drop` for `S` would be automatically generated. – rodrigo Nov 12 '18 at 10:29
  • 1
    @rodrigo Isn't that exactly what the OP was trying to do? – Sven Marnach Nov 12 '18 at 10:52
  • @SvenMarnach: Oh, I see. I was assuming `S` was some composite type. But then the NewType would be exactly like this `S`. – rodrigo Nov 12 '18 at 11:02

1 Answers1

8

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:

Shepmaster
  • 326,504
  • 69
  • 892
  • 1,159
  • Thanks for the explanation! I'm trying to get my head around this, but the call for `assume_init()` does re-activates the drop on `T` right? In which case, `valid_t` still gets dropped at the end of `S::drop()`, I think? Maybe using `assume_init_mut()` prevents the drop from happening? – Thiago Machado Feb 21 '22 at 16:08
  • 1
    @ThiagoMachado yes, `MaybeUninit::assume_init` gives back a `T` which is then subject to normal drop rules. *`valid_t` still gets dropped at the end of `S::drop()`* — it does not, as ownership of the `T` is transferred to `destroy_t`. `T` itself doesn't actually implement a `Drop` that does anything interesting. – Shepmaster Feb 21 '22 at 16:29