Check if closure has any captures and extract fn pointer

Is there any way, with the stable compiler, to implement a function with the signature

fn without_captures<F>(input: F) -> Result<fn(T), F>
    where F: Fn(T)
{ /* ... */ }

which returns Ok(X) iff the closure doesn't capture anything (ZST + 'static) where X is a function pointer that replicates the behavior of the closure, otherwise returns the original closure.

I'm guessing this requires specialization and/or other support from the compiler which is not implemented yet but figured I would ask if anyone has any clever tricks.

I don't think specialization has any business to do with this. Captures don't (directly) show up in trait bounds.


Anyway, here's a hilariously unsafe, don't-even-think-of-using-it-for-anything non-solution, just for the sake of mental exercise:

use core::mem::{size_of, needs_drop, MaybeUninit};

fn without_captures<T, F>(input: F) -> Result<fn(), F>
where
    F: Fn()
{
    if size_of::<F>() == 0 && !needs_drop::<F>() {
        Ok(|| unsafe { MaybeUninit::<F>::uninit().assume_init() }())
    } else {
        Err(input)
    }
}

I have absolutely zero proof to justify summoning the ZST from uninitialized thin air. But hey, you can check if you can get away with it in debug mode.

2 Likes

I think this would need at least both size_of<F>() == 0 && !needs_drop<F>(). (Which, I think specialization came to mind because I was thinking of Copy + 'static, although you may be right it's nothing to do with trait bounds specifically)

1 Like

Reading this right now, seems relevant:

https://github.com/rust-lang/unsafe-code-guidelines/issues/250

1 Like

Yeah, excellent point about needs_drop(). (Maybe it would be more elegant using ManuallyDrop…?)

And who knows how many more soundness holes this has – this is why I very sharply advise against seriously trying to achieve this kind of stuff. (Seems like a strange requirement anyway, it's basically a downcasting anti-pattern.)

In this case it's an optimization where I know almost all of the time, the closure is going to be empty, and I'm bundling a single pointer sized value along with it. Boxing for dyn Trait means a memory representation akin to Box<Box<_>>, with this function I could avoid the extra indirection and undersized allocation.

The compiler does know about captures (or lack thereof) when letting you coerce closures to a fn pointer, but I don't know any way to do this for a generic type parameter.

It seems like, based on this and other similar documentation, appearing a ZST closure with assume_init() is sound, as long as it's 'static + Copy and you keep the Send/Sync bounds in mind.

(I already have a 'static bound but I would still have to figure out how to check a Copy bound without specialization)

Boxing a ZST doesn't allocate.

1 Like

My case is boxing a (Fn(Rc<_>), Rc<_>) to turn into a trait object; with this function I could reimplement the vtable manually and avoid the additional box in my common case of Fn being a Zst, (while still boxing if necessary). It's a specific case optimization somewhat akin to smallstr.