Is the following code sound?
#![feature(maybe_uninit)]
use std::mem;
const N: usize = 2; // or another number
type T = String; // or any other type
fn main() {
unsafe {
// create an uninitialized array
let t: mem::MaybeUninit<[T; N]> = mem::MaybeUninit::uninitialized();
// convert it to an array of uninitialized values
let mut t: [mem::MaybeUninit<T>; N] = mem::transmute(t);
// initialize the values
t[0].set("Hi".to_string());
t[1].set("there".to_string());
// use the values
println!("{} {}", t[0].get_ref(), t[1].get_ref());
// drop the values
mem::replace(&mut t[0], mem::MaybeUninit::uninitialized()).into_initialized();
mem::replace(&mut t[1], mem::MaybeUninit::uninitialized()).into_initialized();
}
}
I should note that miri runs it without problems.
Correction: The answer below still holds in the general case, but in the case of MaybeUninit
there are a few handy special cases about memory layout that makes this actually safe to do:
First, the documentation for MaybeUninit
has a layout section stating that
MaybeUninit<T>
is guaranteed to have the same size and alignment asT
.
Secondly, the language reference says this about array layouts:
Arrays are laid out so that the
nth
element of the array is offset from the start of the array byn * the size of the type
bytes. An array of[T; n]
has a size ofsize_of::<T>() * n
and the same alignment ofT
.
This means that the layout of MaybeUninit<[T; n]>
and the layout of [MaybeUninit<T>; n]
are the same.
Original answer:
From what I can tell, this is one of those things that are likely to work but not guaranteed, and may be subject to compiler-specific or platform-specific behavior.
MaybeUninit
is defined as follows in the current source:
#[allow(missing_debug_implementations)]
#[unstable(feature = "maybe_uninit", issue = "53491")]
pub union MaybeUninit<T> {
uninit: (),
value: ManuallyDrop<T>,
}
Since it's not marked with the #[repr]
attribute (as opposed to for instance ManuallyDrop
), it's in the default representation, of which the reference says this:
Nominal types without a repr attribute have the default representation. Informally, this representation is also called the rust representation.
There are no guarantees of data layout made by this representation.
In order to transmute from Wrapper<[T]>
to [Wrapper<T>]
, it must be the case that the memory layout of Wrapper<T>
is exactly the same as the memory layout of T
. This is the case for a number of wrappers, such as the previously mentioned ManuallyDrop
, and those will usually be marked with the #[repr(transparent)]
attribute.
But in this case, this is not necessarily true. Since ()
is a zero-size type, it's likely that the compiler will use the same memory layout for T
and MaybeUninit<T>
(and this is why it's working for you), but it is also possible
that the compiler decides to use some other memory layout (e.g. for optimization purposes), in which case transmuting will not work anymore.
As a specific example, the compiler may chose to use the following memory layout for MaybeUninit<T>
:
+---+---+...+---+
| T | b | where b is "is initialized" flag
+---+---+...+---+
According to the above quote, the compiler is allowed to do this. In this case, [MaybeUninit<T>]
and MaybeUninit<[T]>
have different memory layouts, since MaybeUninit<[T]>
has one b
for the entire array, while [MaybeUninit<T>]
has one b
for each MaybeUninit<T>
in the array:
MaybeUninit<[T]>:
+---+...+---+---+...+---+...+---+...+---+---+
| T[0] | T[1] | … | T[n-1] | b |
+---+...+---+---+...+---+...+---+...+---+---+
Total size: n * size_of::<T>() + 1
[MaybeUninit<T>]
+---+...+---+----+---+...+---+----+...+---+...+---+------+
| T[0] |b[0]| T[1] |b[1]| … | T[n-1] |b[n-1]|
+---+...+---+----+---+...+---+----+...+---+...+---+------+
Total size: (n + 1) * size_of::<T>()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With