Using MaybeUninit with a generic parameter

The docs for MaybeUninit give the following snippet for working with uninitialized arrays:

let mut data: [MaybeUninit<Vec<u32>>; 1000] = unsafe {
    MaybeUninit::uninit().assume_init()
};

for elem in &mut data[..] {
    elem.write(vec![42]);
}

unsafe { mem::transmute::<_, [Vec<u32>; 1000]>(data) }

My understanding is that this is safe because:

  • the first unsafe block is fine because it's asserting that a MaybeUninit::uninit is already initialized, but this is fine because this is resolving to a type of [MaybeUninit<_>, N], which explicitly doesn't require initialization
  • the second unsafe block is fine because [MaybeUninit<Vec<u32>>; 1000] is guaranteed to have the same size/alignment as [Vec<u32>; 1000], and every element is initialized, so the transmute is fine

I'm trying to extend this pattern to generic types. Roughly speaking, I have a function like this:

fn create_array<T, const N: usize>(f: impl FnMut(usize) -> T) -> [T; N] {
  let mut data: [MaybeUninit<T>; N] = unsafe {
    MaybeUninit::uninit().assume_init()
  }

  for (index, slot) in data.iter_mut().enumerate() {
    let value = f(index);
    slot.write(value);
  }

  unsafe { transmute_copy::<_, [T; N]>(&data) }
}

(I'm using transmute_copy since transmute doesn't seem to work with const generic parameters).

However, I'm worried that this is unsound. In particular, I'm not sure that its sound if f panics. I have written some tests that panic in f and run them with Miri, which doesn't seem to find any issues, but I'm still not certain. A friend pointed me towards the UnwindSafe trait, which seems to suggest that some types can leave data in an inconsistent state when panicking, but it also says that, since it's not an unsafe trait`, memory safety doesn't depend on it being implemented appropriately.

Am I worrying about nothing, or is there an issue when using generics here? Any advice very much appreciated :grin: Thanks!

If f panics, any Ts that have already been returned from f will be deallocated without running their destructors. This is memory-safe, but will leak any resources that would have been cleaned up by those destructors (heap memory, file handles, mutex locks, etc.).

I don’t think UnwindSafe comes into play here, because you only treat values as initialized after you’re sure nothing panicked.

5 Likes

Thanks, that makes sense! Just double checking, if f panics after producing some Ts, it behaves as if those Ts were just passed to std::mem::forget?

Yes

1 Like