Why does the reference to function item type allocates memory?

Here is my code:

use std::mem;
fn main() 
{   
    //size of fcn_ref is 8 bytes (64bit system)
    let fcn_ref = &add;

    // size of fcn_ref2 is 0 bytes
    let fcn_ref2 = add;

    assert_eq!(mem::size_of_val(&fcn_ref2), 0);
    assert_eq!(mem::size_of_val(&fcn_ref), mem::size_of::<usize>());
}

fn add(x: i32, y: i32) -> i32 {
    x + y
}

Here is my reasoning, please correct me if I am wrong.
So the fcn_ref2 will denote the value of an unnameable type that uniquely identifies the function add, and the value of that uniquely unnameable type is zero-sized so no allocation happens.

fcn_ref is a shared reference to the function add specific unique unnameable type. That type is not zero-sized, only the value of that type is zero-sized. As the type is not zero-sized so it allocates memory to save itself, hence reference to it will allocate memory too, as a result, fcn_ref is 8 bytes in 64-bit memory.

1 Like

That type is perfectly nameable: fn(i32, i32) -> i32, which is a pointer to a function. This is an important point: even without the borrow, it's still a pointer to a function.

I can't be 100% sure but I think the value is zero because the pointer points into the binary's memory rather than to the heap or stack.
So indeed no allocation happens there, nor is any necessary.

As for fcn_ref, it is a regular borrow to a value and as such its size is the same as a usize, like your code asserts.

1 Like

In this case, it's not the fn pointer, but the "function item", which is zero-size indeed. It can be coerced to fn(i32, i32) -> i32, but by default it isn't - check this:

fn main() {
    let _: () = main;
}

This errors out with the following:

error[E0308]: mismatched types
 --> src/main.rs:2:17
  |
1 | fn main() {
  | --------- fn() {main} defined here
2 |     let _: () = main;
  |            --   ^^^^ expected `()`, found fn item
  |            |
  |            expected due to this
  |
  = note: expected unit type `()`
               found fn item `fn() {main}`

Note the last line - that's where the unnamable type comes in.

Not sure what do you mean by "reference to the type". References are always to the value, in this case - to the zero-sized value of the unnamable type.

9 Likes

So fcn_ref2 is a function pointer? Function pointer size is not zero.

Exactly, hence it isn't a function pointer.
To put it differently, the function name in that "context" will always be a function-item, which is a zero-sized type (and also unnameable, meaning you cannot really write the type of a function-item).
Btw, I am really writing what @Cerber-Ursi has described in their post, you should read it carefully perhaps.

2 Likes

I do not know that we can't make a reference to a type. So we can tell, following your newly added insight, that fcn_ref is a reference to the value of function item type, and this value is zero-sized. If a value is zero-sized we can assume that no allocation happens, so no memory address too. Then how can we make a reference to such values(zero-sized)?

Rust allows pointers and references to zero-sized types, there just isn't anything at the place they're pointing at.

This doesn't necessarily mean that it allocates any memory. For example, a Box<()> will just be a dangling pointer without an allocation, but it will still take up eight bytes of space.

10 Likes

Thank you, it clarified my confusion.

I am still wondering what is inside of the memory address the fcn_ref (in the code I added with this question) holds?

Because it seems a real memory address(in my computer fcn_ref refers this 0x556be477205b address), not the typical memory address like 0x1 returned by Box::new(zero-sized type).

This doesn't make sense. The size of a value is the size of its type. (Except for dynamically-sized values/types, where it's debatable whether the size of the type can even be defined, but this is unimportant here.)

The difference you are observing is simply that the size of the unnameable fn item is 0, but fcn_ref is itself a reference, so its size is not zero. It is a pointer type, so it is pointer-sized.

2 Likes

The compiler picks an address on the stack for stack variables, even if zero-sized.

4 Likes

But is the function item the "stack variable"?

A function item is effectively just a marker type that names that particular function. It implements Fn, FnMut, FnOnce, Copy, etc., delegating the calls to the actual function address. The function item can be converted to a real pointer by coercing it to a valid fn(...) -> ... type. In your original code, let fcn_ref = &add; actually does two things: it creates a temporary of type AddItem on the stack, then it assigns to fcn_ref a reference to that temporary. let fcn_ref2 = add; just creates another AddItem value on the stack, which also has 0 size.

4 Likes

Yes.

Incidentally, as far as sizes go, function items and non-capturing closures are similar.

3 Likes