Not quite. I'll let T: Zst
mean size_of::<T>() == 0
.
If you own F: Fn() + Zst
, that implementation of Fn
must not rely on the address of self
. As a trivial proof:
let f: F = /* … */;
f();
let f = f;
f();
After moving the value, it gets a fresh address that may or may not be the same address. In fact, since it's a zero sized value, it may be located at any aligned address (other than 0
); there's nothing that fundamentally restricts it to a “stack” address.
As long as you own the zero sized value, it's valid to move it to any aligned address manually, even with a “move” that is just declared to have happened with no source code implementing that movement. The only real thing you need to prevent is that without a Copy
bound it is invalid to have/call the value at some address while it is already being called while located at a different address (this requires copying the value to exist in multiple places simultaneously).
If you have &F
, however, this is not valid unless you have external cooperation. Calling code could also have the same &F
and observe the F
at a different address then where you would like to have it be.
Whether it's possible to name such a type isn't necessary; you can call f(callback)
with any value to get a generic F: Bou+nds
for that value's type.
It is theoretically possible to write a zero-sized closure which cares about its self
address, by move
-capturing a zero-sized value which cares about its address. But such a value wouldn't be pinned, so relying on that address to not change would still be incorrect.
Also, still nothing guarantees that the captured ZST token is actually at the same address as the closure self
, as mentioned above. If you absolutely need a real memory address, you need to have some size.
So &'static impl Fn() + Zst
to fn()
would be valid. As would a theoretical &'a impl Fn() + Zst
to fn() + use<'a>
. But &impl Fn() + Zst
to fn()
isn't valid without a Copy
bound.
It's already the case that captureless closures can be coerced into function pointers. We can do the same for any zero-sized impl Fn
as well. Here's a library-level implementation for fn() -> ()
:
pub fn into_fn<F: Fn()>(f: F) -> fn() {
const { assert!(size_of::<F>() == 0) };
let p = align_of::<F>() as *mut F;
// SAFETY: F is zero-sized; any aligned address is a valid place
// SAFETY: write without dropping non-present value
unsafe { p.write(f) };
|| {
let p = align_of::<F>() as *const F;
// SAFETY: we wrote a value of F to this place above
let f: &F = unsafe { &*p };
f()
}
}
This isn't quite like a zero-captures coercion, since that coercion is a Copy
rather than a move, but the compiler already does move coercion (e.g. Box<T>
to Box<impl Trait>
), so it's just a matter of enabling the coercion, if I understand correctly.