Assigning MaybeUninit::<Foo>::uninit() to a local variable is usually free, even when size_of::<Foo>() is large. However, passing it for example to Arc::new causes at least one copy (from the stack to the newly allocated heap memory) even though there is no meaningful data. It is theoretically possible that a Sufficiently Advanced Compiler could optimize this copy away, but this is reportedly unlikely to happen soon in LLVM.
This issue tracks constructors for containers (Box, Rc, Arc) of MaybeUninit<T> or [MaybeUninit<T>] that do not initialized the data, and unsafe conversions to the known-initialized types (without MaybeUninit). The constructors are guaranteed not to make unnecessary copies.
PR https://github.com/rust-lang/rust/pull/62451 adds:
impl<T> Box<T> { pub fn new_uninit() -> Box<MaybeUninit<T>> {…} }
impl<T> Box<MaybeUninit<T>> { pub unsafe fn assume_init(self) -> Box<T> {…} }
impl<T> Box<[T]> { pub fn new_uninit_slice(len: usize) -> Box<[MaybeUninit<T>]> {…} }
impl<T> Box<[MaybeUninit<T>]> { pub unsafe fn assume_init(self) -> Box<[T]> {…} }
impl<T> Rc<T> { pub fn new_uninit() -> Rc<MaybeUninit<T>> {…} }
impl<T> Rc<MaybeUninit<T>> { pub unsafe fn assume_init(self) -> Rc<T> {…} }
impl<T> Rc<[T]> { pub fn new_uninit_slice(len: usize) -> Rc<[MaybeUninit<T>]> {…} }
impl<T> Rc<[MaybeUninit<T>]> { pub unsafe fn assume_init(self) -> Rc<[T]> {…} }
impl<T> Arc<T> { pub fn new_uninit() -> Arc<MaybeUninit<T>> {…} }
impl<T> Arc<MaybeUninit<T>> { pub unsafe fn assume_init(self) -> Arc<T> {…} }
impl<T> Arc<[T]> { pub fn new_uninit_slice(len: usize) -> Arc<[MaybeUninit<T>]> {…} }
impl<T> Arc<[MaybeUninit<T>]> { pub unsafe fn assume_init(self) -> Arc<[T]> {…} }
PR https://github.com/rust-lang/rust/pull/66128 adds:
impl<T> Box<T> { pub fn new_zeroed() -> Box<MaybeUninit<T>> {…} }
impl<T> Arc<T> { pub fn new_zeroed() -> Arc<MaybeUninit<T>> {…} }
impl<T> Rc<T> { pub fn new_zeroed() -> Rc<MaybeUninit<T>> {…} }
Unresolved question:
Box<MaybeUninit<T>> might “belong” more as an associated function of that same type, rather than Box<T>. (And similarly for other constructors.) However this would make a call like Box::<u32>::new_uninit() becomes Box::<MaybeUnint<u32>>::new_uninit() which feels unnecessarily verbose. I suspect that this turbofish will be needed in a lot of cases to appease type inference.Just from looking at the current source code:
pub fn new_uninit() -> Box<mem::MaybeUninit<T>> {
let layout = alloc::Layout::new::<mem::MaybeUninit<T>>();
let ptr = unsafe {
Global.alloc(layout)
.unwrap_or_else(|_| alloc::handle_alloc_error(layout))
};
Box(ptr.cast().into())
}
When mem::size_of::<T>() == 0, a zero-sized layout is passed to Global.alloc but alloc mentions:
This function is unsafe because undefined behavior can result if the caller does not ensure that layout has non-zero size.
Did I have overseen something or is this a bug?
Good point! I copied this pattern from Rc and Arc, but there the header (refcounts) cause the allocation to never be zero-size.
Box::new_uninit_slice has a similar bug with size_of() == 0 or len == 0.
I have noticed this when implementing Box for custom allocators with non-zero layouts. Turns out, that this is indeed useful :slightly_smiling_face:
This is my implementation for the sliced version.
What are the requirements for stabilizing the Box API?
I don't think using Rc<MaybeUninit<T>> (and Arc) is currently that useful, as Arc happens to be shared (no mutation). However, there's this pattern where one wants to create a value while it's uniquely owned and then share it later. Doing it with Box would mean copying, so I was thinking about having RcMut and ArcMut that work exactly as Box, but reserve space for reference counters, so that when you call share() on them, they just initialize ref count and convert to Rc/Arc. This is however another topic. Did anyone had this idea before or should I start discussion somewhere?
there's this pattern where one wants to create a value while it's uniquely owned and then share it later
Yes, that’s exactly the idea with having Rc::new_uninit together with Rc::get_mut_unchecked https://github.com/rust-lang/rust/issues/63292. It would also work with Rc::new_uninit + Rc::get_mut + unwrap, with an unnecessary run-time check (assuming sharing only after this).
Great! I still think it'd be better to provide safe abstraction for it, but at least it'd be possible to do in a library.
Similar to new_zeroed() it would be nice to have a new_zeroed_slice().
If anyone is searching for a replacement for <_>::new_uninit_slice, the following code seems to produce the same asm as unstable method :)
iter::repeat_with(|| MaybeUninit::<T>::uninit()) // you can use `repeat` in case `T: Copy`
.take(20)
.collect::<Wrapper<[_]>>() // Wrapper is Box/Rc/Arc
(godgolt)
Most helpful comment
If anyone is searching for a replacement for
<_>::new_uninit_slice, the following code seems to produce the same asm as unstable method :)(godgolt)