See if you can follow my train of thought here:
fn main() {
// non-capturing closure doesn't capture at all
// (as per the ... duh?), which gives the compiler
// a chance to treat it as a ZST-like `fn()` which,
// conventionally, are never placed/allocated
// either on the stack or the heap at all:
let fn_closure = || println!("no capture");
// the `size_of` of it is trivially zero:
assert_eq!(size_of_val(&fn_closure), 0);
// and our `no_alloc` never fails
let fn_box = no_alloc(fn_closure);
// now compare it to this one,
// where we are capturing by `move`:
let capture = String::from("text");
// by the point the compiler finishes constructing
// our closure, it will have created (conceptually)
// some tuple/struct of the form `(String, fn(&String))`,
// where the `fn(&String)` alone can *still* be treated as a ZST,
// but the `String` itself *must* be "captured" for the closure to work
let fn_capture = move || println!("captured: {capture}");
// the size of it must be equal to the size of the state being captured:
assert_eq!(size_of_val(&fn_capture), size_of::<String>());
// and our `const` + `panic!` check doesn't even let
// our program compile if we wanted it to:
let fn_box_cte = no_alloc(fn_capture);
}
I'm assuming you mean the size_of_val(fn_box)? As the size_of_val(f) is either:
// for the first closure
size_of_val(&f) = 0
// for the second one
size_of_val(&f) = 24
The fn_box is 16 bytes large (and 8 bytes aligned) because the underlying pointer is. Box<T> is just a wrapper around a Unique<T>, which is itself a wrapper around NonNull<T>, which is - yes, you've guessed it - another wrapper yet, around a plain raw pointer to *const T this time:
// "go to definition" on any `Box` in your IDE:
pub struct Box<T: ?Sized, A: Allocator = Global>(Unique<T>, A);
// go to `Unique<T>`:
pub struct Unique<T: PointeeSized> {
pointer: NonNull<T>,
_marker: PhantomData<T>,
}
// go to `NonNull<T>`:
pub struct NonNull<T: PointeeSized> {
pointer: *const T,
}
Therefore, since our fn_box is indeed a Box<dyn Fn()>, a size_of_val(&fn_box) is, in fact, same as size_of::<*const dyn Fn()>; and the reason the a *const dyn Fn() is 16 bytes large, instead of the usual 8 bytes for a regular pointer, is because of what's required to refer to any trait object out there: two pointers, meshed together, passed around as one (a "fat" pointer).
The first references the underlying state (the captured String), the other one - the function (the fn(&String) in this case) being called. Except that this last part is not quite right, either: you can only "get" to that function by looking it up in the "virtual table" first. It references all of the available methods for that particular trait object type, and it's this vtable reference that is actually being stored in the slot of the second pointer for the *const dyn Fn(), wrapped 3 times over into a Box.
It's just another way for enforcing certain conditions at compile-time.
fn no_alloc<T>(f: T) -> Box<T>
{
// this is not allowed, and will blow up with:
// `error[E0401]: can't use generic parameters from outer item`
const VALIDATE: () = {
if size_of::<T>() > 0 {
panic!("only non-capturing `Fn()` are allowed")
}
};
// this is allowed, and will only run *if* the `no_alloc` itself
// is ever referenced/called, at any point, in your actual source code
const {
if size_of::<T>() > 0 {
panic!("only non-capturing `Fn()` are allowed")
}
};
Box::new(f)
}