As noted, the Box allocate on heap for value.
But should we need to create a value on stack and then copy to the box?
For example,
struct Foo {
val: i32,
}
impl Drop for Foo {
fn drop(&mut self) {
println!("drop me");
}
}
fn main() {
let a = Foo { val: 1 };
let aa = Box::new(a);
println!("{}", aa.val);
}
a is Foo type and allocated on stack.
Then box it, which move a to the heap, correct?
So, how to create value in box directly? Or the compiler could check this case and optimize it (I check the Box::new, it uses box t. What's the black-magic box?)
This is to the best of my knowledge, but may be wrong:
Yes.
Yes.
I don't know how the compiler optimizes this.
How would you "create value in box directly" ? This sounds like doing a malloc then assigning to the fields. However, in Rust, I'm not sure if it's safe / defined behaviour to have uninitialized fields/variables.
I might be that you have a different model for "create value in box directly" in mind.
AFAIK right now compiler usually does not optimize it, which can cause stack overflows. box is a keyword introduced by RFC 0809. If you want to create value directly on the heap, I think it will be better to use Box::new_uninit followed by assume_init after initialization is complete.
A distinction must be drawn between the abstractions of the language and how these may be implemented.
Yes, every function parameter is first a "local", and yes, most locals are implemented as stack variables. But when the optimizer kicks in, it can inline / reorganize most of this stuff. So, for instance, both your local a, or my anonymous local can be fed directly to the functions consuming it, without needing to construct the value in the stack beforehand.
However, the optimizer can only do changes that do not affect the semantics of the program. And one very hidden semantic of Box::new(<expr>) is that:
A local <expr> is created;
An allocation to hold it is attempted;
Then the value is moved to the heap,
But if 2. fails (e.g., out of memory), then it may panic, so this local must be dropped.
And even it instead of panic!-king it aborted, then since the creation of <expr> might have had side-effects it needs to happen whatever the result of the allocation.
This prevents an optimizer to inline / move the creation of the struct directly into its backing (heap) allocation.
The ::copyless idiom, Box::alloc().init(<expr>), expresses the following semantics:
Box::alloc()
An allocation is attempted;
if it fails, then panic / abort; but since <expr> does not exist yet, there is no interaction with that whatsoever.
.init(<expr>)
Create a local <expr>;
Move that value into the heap. // <- this cannot fail
With these semantics the optimizer is allowed to inline the creation of <expr> directly into the heap.
MaybeUninit can't know if its contents are initialized, so it doesn't drop the contained object (as dropping garbage memory would be undefined behavior). If you look at its definition, you can see that it holds the wrapped value as a ManuallyDrop<T>.
impl<T> BoxAllocation<T> {
/// Consumes self and writes the given value into the allocation.
#[inline(always)] // if this does not get inlined then copying happens
pub fn init(self, value: T) -> Box<T> {
They may panic in the future, so I just stay open-minded to both possibilities now.
This is Undefined Behavior; since the pattern involving MaybeUninit can be misused, I suggest favoring ::copyless, which basically wraps this pattern in a non-unsafe API.
As @RustyYato said, these optimizations are hard to guarantee from a library author perspective, but by hinting the optimization to the compiler it is very likely they will happen. And indeed, the #[inline(always)] is such a hint.