What does Box<dyn ...> actually allocate?

Hi there!

I'm currently struggling to understand what exactly is Box<dyn ...> allocating?

Using std::mem::size_of, I always get the same result for any dynamic type as it is based on the type itself. Same for values if I provide the type of a closure, which gives the following result:

assert_eq!(std::mem::size_of::<Box<dyn FnOnce()>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn FnMut()>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn()>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn(usize)>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn() -> bool>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn(usize) -> bool>>(), 16);

More strangely though, getting the size of a closure from value always returns 0 (surely I'm doing something right here):

fn size_of_value<T>(_: &T) -> usize {
    std::mem::size_of::<T>()
}

let closure = |path: std::path::PathBuf| path.to_string_lossy().is_empty();

assert_eq!(size_of_value(&closure), 0); // What?

So I was wondering, first why is the second example not working as expected :stuck_out_tongue: and then how much space a Box<dyn ...> will actually take in memory, and not just on the stack, if I provide it a very large closure. Where will the different parts of the data be stored, the parts being:

  • The function's content (instructions) which I suppose remain in the program's binary and won't require an allocation each time ;
  • The function's pointer to indicate which should be called (which I suppose is part of the Box type);
  • The informations required for dynamic dipatch (which I suppose is part of the Box type)

Thanks in advance for your help :slight_smile:

EDIT: Re-reading this post I realize my goal isn't clear here ; I'm wondering if I can use Box with a very large closure, without it requiring a big heap allocation.

Yeah, so the size_of thing you are doing at the top of the post just measures the stack size of the Box, which is 16 because it is both a pointer and a vtable.

Additionally, it's completely correct that some closures have the size zero. This is because they compile down to a unique anonymous type whose call method has the contents of the closure. Of course, if you cast it to a function pointer, or have the closure capture a value, then it will no longer be size zero.

As for the size of an allocation of a Box<dyn Trait>, well it varies. It has the same size as the object you put inside it, so if you put a zero-sized closure into it, it doesn't allocate, but if the closure has captured a bunch of variables, then it could be larger.

Thanks for your answer!

So you're saying that for closures, as long as I use functions that don't capture anything from their environment, they'll always be compiled down to ZST and as such putting them into Box<dyn ...> won't require heap allocation, just stack allocation for storing the dynamic dispatch informations + the pointer?

It allocates a single contiguous buffer dynamically ("on the heap" if you prefer that terminology) for its wrapped value.

size_of::<Box<_>> is the size of the box, not the size of its heap buffer. The size_of function returns the size of its type parameter. Apart from necessarily being a compiler intrinsic, this function is not magic – it doesn't have special knowledge as to what potential heap buffers a type might allocate and point to. It only gives you the size of that type itself, which for a box is exactly one pointer (for Sized types) or two pointers (for dynamically-sized types).

If you are interested in the size of the contained type, ask for size_of::<T>() instead of size_of::<Box<T>>() for Sized types and size_of_val::<T>() for unsized types. If you are interested in approximate heap sizes, use the heapsize crate.

Correct.

Almost. A Box<dyn Trait> is made of two pointers:

  • one to the data (which points to no allocated memory if the type behind the trait is zero-sized);
  • and another one to the vtable required for dynamic dispatch.

In the case of a zero-sized closure, there's nothing to store for the data part, not even a function pointer. Calling closures is realized by means of the call_once(), call_mut(), etc. methods of the various Fn* traits, so the actual function pointer goes into the vtable part. This is also true when you don't use dynamic dispatch: unless you explicitly convert a closure to a function pointer, it will be statically dispatched and there isn't any function pointer business going on.

Finally, @Alice has already explained why some closures are zero-sized. If you capture variables, then you can indeed make non-zero-sized closures, in which case a very big closure can cause a very big heap allocation.

Thanks for your answer :slight_smile:

So this means that, as long as the closure doesn't capture its environment, I can make it as big as I want, wrapping it inside a Box<dyn Fn...> won't require any heap allocation, right?

You mean its body, the actual executable code? Yes, that isn't part of the allocated buffer. Closures only need space for their captured environment.

Perfect then!

Thanks for your help :smiley: