-
Notifications
You must be signed in to change notification settings - Fork 22
Description
Proposal
Problem statement
It is a common need to require something to run when a value gets destroyed. For example to clean up some resource, or to restore some invariant. This is especially important when writing panic-safe code.
Rust has a tool in the language for this purpose: the Drop
trait. This allows you, for a specific type, to specify what should happen when a value of this type gets destroyed. However, this only allows one such implementation per type. This leads to a common but unwieldy pattern where one defines a dummy type wrapping some data for the sole purpose of adding a Drop
implementation to the data.
This pattern gets doubly annoying when generics are involved, as an inner struct definition may not access the generics of its surroundings, forcing you to repeat all those again.
Motivating examples or use cases
There are many such cases of this, even in the standard library. For example in library/alloc/src/collections/btree/mem.rs
:
struct PanicGuard;
impl Drop for PanicGuard {
fn drop(&mut self) {
intrinsics::abort()
}
}
let guard = PanicGuard;
...
mem::forget(guard);
Or compiler/rustc_serialize/src/opaque.rs
:
struct SetOnDrop<'a, 'guarded> {
decoder: &'guarded mut MemDecoder<'a>,
current: *const u8,
}
impl Drop for SetOnDrop<'_, '_> {
fn drop(&mut self) {
self.decoder.current = self.current;
}
}
Or library/std/src/sys/pal/unix/sync/condvar.rs
:
struct AttrGuard<'a>(pub &'a mut MaybeUninit<libc::pthread_condattr_t>);
impl Drop for AttrGuard<'_> {
fn drop(&mut self) {
unsafe {
let result = libc::pthread_condattr_destroy(self.0.as_mut_ptr());
assert_eq!(result, 0);
}
}
}
Or an example of repeating generics from library/alloc/src/vec/drain.rs
:
struct DropGuard<'r, 'a, T, A: Allocator>(&'r mut Drain<'a, T, A>);
impl<'r, 'a, T, A: Allocator> Drop for DropGuard<'r, 'a, T, A> {
fn drop(&mut self) {
// ...
}
}
Solution sketch
I propose we add a parallel to the core::mem::ManuallyDrop
type to control the drop implementation of a specific value, rather than a type. This type which I propose we call WithDrop
(also in core::mem
) is similar to ManuallyDrop
, but rather than foregoing the drop it calls a specific function on Drop
with the value instead. To be concrete:
#[derive(Clone)]
pub struct WithDrop<T, F: FnOnce(T) = fn(T)> {
value: ManuallyDrop<T>,
on_drop: ManuallyDrop<F>,
}
impl<T, F: FnOnce(T)> WithDrop<T, F> {
/// Wrap a value so that it is passed to `on_drop` when this `WithDrop` is dropped.
#[must_use]
pub const fn new(value: T, on_drop: F) -> Self {
Self {
value: ManuallyDrop::new(value),
on_drop: ManuallyDrop::new(on_drop),
}
}
/// Return the contained value without calling `on_drop`.
pub fn into_inner(self) -> T {
unsafe {
let mut slf = ManuallyDrop::new(self);
let ret = ManuallyDrop::take(&mut slf.value);
ManuallyDrop::drop(&mut slf.on_drop);
ret
}
}
}
impl<T, F: FnOnce(T)> Drop for WithDrop<T, F> {
fn drop(&mut self) {
unsafe {
let val = ManuallyDrop::take(&mut self.value);
let on_drop = ManuallyDrop::take(&mut self.on_drop);
on_drop(val);
}
}
}
impl<T, F: FnOnce(T)> Deref for WithDrop<T, F> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &T {
&self.value
}
}
impl<T, F: FnOnce(T)> DerefMut for WithDrop<T, F> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut T {
&mut self.value
}
}
impl<T: fmt::Debug, F: FnOnce(T)> fmt::Debug for WithDrop<T, F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("WithDrop").field(&self.value).finish()
}
}
With this type the above patterns are easy to implement. For example:
let abort_on_panic = WithDrop::new((), |_| intrinsics::abort());
...
abort_on_panic.into_inner();
Because WithDrop
implements Deref
and DerefMut
you can still easily access the underlying data:
let mut file = std::fs::File::create("foo.txt")?;
...
{
let mut f = WithDrop::new(&mut file, |f| f.flush().unwrap());
write!(f, "important data")?;
may_panic();
write!(f, "more important data")?;
}
Alternatives
There are two main alternatives to this proposal. One is some kind of defer
keyword, e.g. discussed here. That would require full-on language support, and is also limited to strictly scoping-based cleanup. The latter means that even if we later end up accepting some kind of defer
language mechanic in Rust, WithDrop
isn't necessarily obsolete as it allows moving the entire WithDrop
around to do non-scoped per-value cleanup.
The second alternative is simply for a user to implement the above, or use one of the crates that provides this or something similar. The most popular crate providing something similar is the scopeguard
crate. While acceptable to some both feel unsatisfying to me. Adding a custom dummy Drop
struct is annoying but just isn't annoying enough for me to justify adding a dependency on a microcrate. I personally feel if the need is fairly universal† and the implementation simple and uncontroversial it should be part of the standard library.
†The scopeguard
crate has 385 million downloads all-time. It is in the top 50 most downloaded crates of all time.
While this proposal is inspired by scopeguard
, I simplified it to its core: simply associating a drop function with some data. I did not include the macros it contains or the panicking strategies it exposes. I find the latter simply unnecessary as you can call std::thread::panicking
yourself in the drop handler to do something different based on whether or not we're unwinding. To be specific for the former, scopeguard
also contains the defer!
macro (and variants for panicking strategies), which is (paraphrased) defined as
macro_rules! defer {
($($t:tt)*) => {
let _guard = WithDrop::new((), |()| { $($t)* });
};
}
While it is able to solve some of the same problems as this proposal I believe a macro like this is less flexible than the type (it's limited to the current scope, might run into borrowing issues, can't cancel the defer
like WithDrop::into_inner
can), and clashes with a potential future defer
language feature. Should the defer
language feature ever officially be considered and properly denied I wouldn't be fully opposed to adding it in addition to WithDrop
, but I think it makes for a poor alternative.
As for alternatives within this proposal, I considered the names CustomDrop
or OnDrop
as well. I preferred WithDrop
, but I'm open to suggestions.
Links and related work
Defer discussion: https://internals.rust-lang.org/t/a-defer-discussion/20387
scopeguard
: https://docs.rs/scopeguard/latest/scopeguard