Idea: `drop_guard` or `scope_guard` helper function

As the drop guard pattern is widely used in unsafe Rust. Is it worth to add a helper function drop_guard or scope_guard in core::mem that takes a FnOnce() closure to execute on drop?

#[must_use]
pub fn drop_guard(f: impl FnOnce()) -> impl Drop {
    use std::mem::MaybeUninit;
    
    struct DropGuard<F: FnOnce()>(MaybeUninit<F>);
    impl<F: FnOnce()> Drop for DropGuard<F> {
        fn drop(&mut self) {
            // SAFETY: self.0 is initialized.
            let f = unsafe { self.0.assume_init_read() };
            f()
            // f dropped here
        }
    }
    DropGuard(MaybeUninit::new(f))
}

The we could have

    let _guard = drop_guard(|| println!("drop0"));
    let _guard = drop_guard(move || {
        let _guard = drop_guard(|| println!("nested drop"));
        println!("drop1");
    });
    println!("before panic");
    panic!("panic");
    println!("after panic");

Playgound.

5 Likes

I suggest trying this with more complex things than prints. If the closure holds a reference to something, as it generally needs to in order to do something useful, then it'll keep you from using it for anything else.

So the API generally needs to be more complex than just taking a closure. Perhaps it derefs to some state, for example.

(Also, you'd be fine with just ManuallyDrop in your example. No need for MaybeUninit.)

The crate that does this:

8 Likes

Looks cool!

A language built-in could integrate with the borrow checker better. Currently user code is unable to move any objects into the drop guard code and use them outside of the drop guard at the same time. It should be doable and safe, but needs cooperation of the compiler.

1 Like

The sound cases of this are already sufficiently covered by DerefMut.

    let mut guard = guard(Vec::new(), |mut v| v.truncate(1));
    let vec: &mut Vec<_> = &mut *guard;

I don't see which meaningfully different usage a language integration could provide. Maybe you could give an example?

Unfortunately, I can't remember the exact case where I got stuck with it. I've stopped using scopeguard a while ago, because it's too cumbersome. I wanted something like defer || vec.truncate(1);, instead of repeating the variable name 5 times.

The interesting case where scopeguard gets gnarly is when your defer wants to capture multiple bindings, because then you effectively have to pack/unpack them into a tuple.

The simple case of "truncate vec at end of scope" can be written as just

let mut vec: &mut vec; // from before
let mut vec = guard(vec, |vec| vec.truncate());

Which definitely isn't ideal stuttering, but works simply enough. Of course, it also relies on deref coersions, so if you're using it in a place without them, you'll need to add an explicit reborrow.

The advantage of a compiler solution (in that it can "just" move the code block to the end of scope, and not do any closure captures) is definitely interesting to think about.

3 Likes

So the case that's cumbersome with a scopeguard is when the guard needs to borrow, but not move. In some trivial cases you can borrow the value and wrap the scopeguard in another pair of {} to end scopeguard's borrow, but if you need early exits from the function it gets extra tedious:

fn foo() -> Vec<i32> {
    let mut x = Vec::new();
    
    defer ref || x.push(2);
    
    if false { /* defer should run here */ return x; }

    loop {
        x.push(1);
        // defer should run here
        return x;
    }
}

BTW, in case it's not clear why I'm bringing this up, and how it relates to the original proposal: there's already the scopeguard crate. I think additions to std should have a high bar, and be reserved for things that can't be done well by 3rd party crates. Mere convenience of not using a 3rd party crate itself is a weak reason. But if a built-in scopeguard could be more powerful and/or have substantially nicer syntax than a macro, then it'd be a good reason to adopt the feature "natively" in Rust.

2 Likes