Cannot transmute between types of different sizes

I managed to distill my problem to this minimal uncompileable snippet:

struct MyStruct<D> {
    x: D,
}

impl<D> MyStruct<D> {
    fn new() -> Self {
        let x: MaybeUninit<Self> = MaybeUninit::uninit();
        // initialization is omitted for brevity
        let y: Self = unsafe { std::mem::transmute(x) };
        y
    }
}

And I get the compiler error on std::mem::transmute:

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
  --> src/main.rs:97:32
   |
97 |         let y: Self = unsafe { std::mem::transmute(x) };
   |                                ^^^^^^^^^^^^^^^^^^^
   |
   = note: source type: `MaybeUninit<MyStruct<D>>` (size can vary because of D)
   = note: target type: `MyStruct<D>` (size can vary because of D)

Could anyone help me understand why D size might vary, if all generic parameters are Sized by default? And what can I change in code to make this work?

UPDATE: Obviously, I omitted the part where I initialize the initially uninitialized data. Also, I am aware about methods like assume_init, but it does not help me in case when I need to create the fixed-sized array [MaybeUninit<MyStruct<D>>; K] and after initialization - transmute it to [MyStruct<D>; K]. While initial problem from my code sample can be resolved in other ways - I still am trying to understand what's wrong with the approach as it is.

currently, you cannot assert a type is inhabitted using bounds, but MaybeUninit is always inhabitted because it is a union. so the transmute() is unsound. e.g.:

enum Void {}
let void = MyStruct::<Void>::new();

you should use MaybeUninit::assume_init() instead, which will panic in case the type is uninhabitted.

D might be u8 or u32 or etc etc, and they all have different sizes, making MyStruct<D> and MaybeUninit<MyStruct<D>> change sizes too. The two always end up having the same size as each other, but they are "dependently sized", and the compiler can't reason about this.

transmute_copy does not have this check, but your code is far from being correct even with that due to transmuting uninitialized data to some arbitrary type.

@nerditation @SkiFire13 I have updated my initial question. Hoping for answers with additional context :slight_smile:

#[test]
fn uninit_transmute() {
    use std::mem::MaybeUninit;
    struct MyStruct<D> {
        x: D,
    }
    impl<D> MyStruct<D> {
        fn new(d: D) -> Self {
            let mut x: MaybeUninit<Self> = MaybeUninit::uninit();
            // initialization example, for testing purposes
            x.write(Self { x: d });
            unsafe { 
                // for D: Sized, raw ptrs are always the same size == usize
                let y: *mut Self = std::mem::transmute(&raw const x); 
                std::ptr::read(y)
            }
        }
    }
    let my = MyStruct::new(42);
    assert_eq!(my.x, 42);
}

note, this error only ocurrs in generic context, because it's not possible check if the generic type is inhabitted or not. you can perfectly do the transmute() for concrete types.

as I mentioned above, MaybeUninit is a union, MaybeUninit<T> is conceptually a union of an uninitialized (zero-sized) placeholder and an initialized value. in rust, the size of a union is the largest of all the members.

for inhabitted types, the size of a type is the logarithm of the type's cardinality, in units of bits, because computer represented values in binary bits. however, for uninhabitted type, the size technically doesn't have a well defined value, or you can say it should be negative infinity if you extrapolate the base 2 logirithm idea.

although practically, if you query std::mem::size_of() of an uninhabitted type, you get 0, but I don't think it is "correct" from a type theory point of view.

I'm not into the compiler, I don't know how the type checker is actually implemented, but I'm pretty sure it treats uninhabitted types differently than zero-sized types. if this is the case, then MaybeUninit<T> can have different size from T, when T is uninhabitted, in which case, MaybeUninit<T> is zero sized, while T is uninhabitted with a unspecified size.

however, this detail is invisible from the user, std::mem::size_of() will always return the same value for MaybeUninit<T> and T, I guess it's because size_of() needs to return a usize, it must return some value, but there's no sensible value for uninhabitted types.

one possible way to solve the issue is to add a new (compiler implementable only) trait for inhabitted types. another possibility is to change MaybeUninit such that it is UB to use uninhabitted type with it, which should guaranteed that MaybeUninit<T> and T always have the same size, but this would be a very big breaking change, what's more, since the assume_init() already API exists, there's no real benefit of it.

there are unstable APIs for this use case, unfortunately, the only way to do it in stable is to manually transmute the array, but apparently, it doesn't work in generic context.

one way to workaround it is to implement your own version of transmute(), but unlike std::mem::transmute(), when T and U has potential different size, it still compiles:

/// safety: same as `std::mem::transmute()`
const unsafe fn my_transmute<T, U>(x: T) -> U {
	assert!(std::mem::size_of::<T>() == std::mem::size_of::<U>());
	let x = ManuallyDrop::new(x);
	// I use raw pointer in this example
	// alternatively, can use `transmute_copy()`
	let p = &raw const x;
	unsafe { std::ptr::read(p.cast()) }
}

The one-liner for this is to, rather than transmute(x), do transmute_copy(&ManuallyDrop::new(x)).