Why does this code fail to compile?

Code:

fn foo<const N: usize, T: Sized>(x: [MaybeUninit<T>; N]) -> [T; N] {
    unsafe { mem::transmute::<[MaybeUninit<T>; N], [T; N]>(x) }
}

Compile error:

  = note: source type: `[MaybeUninit<T>; N]` (this type does not have a fixed size)
  = note: target type: `[T; N]` (this type does not have a fixed size)

The example code is from the "init array element-by-element" example from MaybeUninit in std::mem - Rust but for some reason, I can not use the last line (transmute) w/ a generic T ?

If you replace the N with an actual constant, you get a slightly more informative error message:

  = note: source type: `[MaybeUninit<T>; 32]` (size can vary because of T)
  = note: target type: `[T; 32]` (size can vary because of T)

So, it looks like the relevant pass of the compiler simply can't prove that MaybeUninit<T> has the same size as T for every possible T:Sized.


You can write foo like this:

unsafe fn foo<const N: usize, T: Sized>(x: [MaybeUninit<T>; N]) -> [T; N] {
    let x = mem::ManuallyDrop::new(x); // Probably not necessary given `MaybeUninit`
    mem::transmute_copy::<[MaybeUninit<T>;N],_>(&*x)
}

Does this incur an extra copy? transmute_copy in std::mem - Rust says:

It will also unsafely create a copy of the contained value instead of moving out of src .

This is part of a deserialization routine, and I'd prefer to avoid this if possible.

One thing I was thinking was trying to do ptr shenanigans since we're already dealing with unsafe. I.e.

[MaybeUninit<T>; N] -> some type of ptr over MaybeUnini<T> -> some type of ptr over T -> [T; N]

See this issue: Const generics: Generic array transmutes do not work · Issue #61956 · rust-lang/rust (github.com)

DutchGhost:
Transmuting a MaybeUninit<T> to T is already rejected, and thus transmuting from any wrapper containing a MaybeUninit<T> to a Wrapper<T> is also rejected.

Yes, [MaybeUninit<T>; N] to [T; N] should be okey, but Option<MaybeUninit<T>> to Option<T> isnt in all cases.

2 Likes

The ManuallyDrop::new() technically does add a copy/move, but it will almost certainly be removed by the optimizer.

transmute_copy itself isn't any worse than transmute, though. It takes a reference as its argument and then copies from behind that reference whereas transmute requires the argument to be moved in the first place— The only real performance difference is whether the memcpy happens before or inside the transmute* call.

Ha! It has the ptr cast i wanted:

// Using &mut as an assertion of unique "ownership"
let ptr = &mut arr as *mut _ as *mut [T; N];
let res = unsafe { ptr.read() };
core::mem::forget(arr);
res

:slight_smile:

@2e71828 : Are you saying that all 3 solutions (transmute, transmute_copy, ptr manip) involve 1 copy/move. I.e. the pointer manip solution does the "copy" at the ptr.read() line ?

Yes, unless I'm missing one somewhere— A move operation is equivalent to copy+forget, so I'd be very surprised if these solutions didn't all end up compiling to the exact same machine code (especially with optimizations turned on).

1 Like