(&mut T, &dyn Fn(T) -> (T, W) ) -> W?

Is there a safe way to impl a function

g: (v: &mut T, func: &dyn Fn(T) -> (T, W)) -> W

the way g behaves is:

(1) we abuse v: &mut T into a v: T, pass it to func, then store the T back, while returning the W.

Is there a sound way to do this ?
u
If yes, how?

If no, why ?

EDIT: We do not have Default / Clone on T

EDIT:

it is possible to avoid UB on unwinding. see comments below.


original answer

I think the answer is no. the main reason is unwinding. although you can unsafely read out the value of v temporarily (e.g. using raw pointer ptr::read()), leaving the old memory semantically "uninitialized", what happens if func panics? even if you can catch the panic with catch_unwind, what value do you put back to v?

2 Likes
v: &mut T
f: |x: T| -> (T, W) { ... }

so now we are in the stackframe of f , we have:

f's stack frame:
x: T
====
some other frame, v: &mut T
====

so we have a panic now, T::drop(&mut x) is called

then as we unwind, eventually whatever the v: &mut T pts to also has T::drop(&mut self) called on it

^-- Is this the argument you are stating ?

eventually, yes. but that's not all the problems it could cause. if, for example, somewhere in the call stack, there's a catch_unwind() to stop the unwind, but before the frame which owns the original value of v, we now have a invalid reference &mut T, so it is possible to be accessed even before its destructor.

that's just what first comes to my mind as for the consequences, there might be more reasons to it, but I'm pretty sure the answer is no anyway.

1 Like

See the take_mut crate. It can do it.

2 Likes
pub fn take<T, F>(mut_ref: &mut T, closure: F)
  where F: FnOnce(T) -> T {
    use std::ptr;

    unsafe {
        let old_t = ptr::read(mut_ref);
        let new_t = panic::catch_unwind(panic::AssertUnwindSafe(|| closure(old_t)))
            .unwrap_or_else(|_| ::std::process::abort());
        ptr::write(mut_ref, new_t);
    }
}

Mechanically how does this work? We catch the panic, call abort, meaning no further drop handlers (on "lower stack frames") are called ?

Is the key distinction

unwind == process of popping the remaining frames + calling drop handlers
abort == instant death

?

Thus, the abort prevents the T::drop from being called twice ?

Yes that’s correct.

There’s also the crate replace_with - Rust with some minor differences such as better defaults (the easier name goes to a function which also requires some fallback behavior, and the version that aborts is “replace_with_or_abort”) and a slightly different (probably more efficient) mechanism to handle the panic case (via a destructor [Drop implementation], instead of catch_unwind).

2 Likes