Missing layout optimization for types containing Infallible /!

The compiler understands that if an enum variant only contains an uninhabited type the variant cannot be constructed (if it's one of two variants of an enum, the discriminant will be elliminated). Existence of other fields, however, breaks this. Consider this type:

enum Test<T> {
    One,
    Two { large: [u8; 2048], never: T },
}

The size of Test<Infallible> and Test<!> (with the feature enabled) is 2049 (meaning it reserves space for both the data carried by the variant Two, and a byte for the discriminant), while it could be 0. Additionally, any structures containing an uninhabited type should be zero-sized, which is not the case.

Is this a feature request, or should I open an issue in the Rust repository under some other category?

4 Likes

(Not commenting on the enum part here.)

This has been discussed, and is by design -- it's not changing.

In short, the problem is that even though (u64, !) is uninhabited, MaybeUninit<(u64, !)> is inhabited. And it's been decided that preserving that size+align equality is more important that letting structs like that be zero-size -- especially for unsafe code that does projections.

After all, any codepath that would have a valid (u64, !) is necessarily dead anyway.

5 Likes

Can you please link that discussion with a decision? Imho size_of::<MaybeUninit<(u64, !)>>() should be 0, as well as size_of::<Option<(u64, !)>>()...

1 Like

I unfortunately don't have any links to provide, but it's largely because of partial initialization, e.g. consider

struct S<T>(u64, T);

fn new<T>(f: impl Fn() -> T) -> Box<S<T>> {
    let mut b = Box::<S<T>>::new_uninit();
    let mut p = b.as_mut_ptr();
    unsafe {
        (&raw mut (*p).0).write(0u64);
        (&raw mut (*p).1).write(f());
        b.assume_init()
    }
}

It'd be extremely unfortunate if this were to be UB for uninhabited T.

10 Likes

And perhaps more discussion if you link-chase from here.

3 Likes

This doesn’t necessarily mean anything for (#[repr(Rust)]) enums though, which is relevant at least for the code example in @kamirr's original post

see also:

2 Likes

well, Rust seems likely to gain support for constructing enums in a similar fashion, by writing each field to a MaybeUninit<TheEnum<T>> and then setting the discriminant. That would ran into the exact same issue if Rust treated uninhabited variants as zero-size.

I don't think there's any pre-existing differences between a type with a generic and an otherwise identical type without a generic, but it seems like it could be done in such a way that any non-generic struts be ZST. It would be odd, admittedly, but in theory it could work?

Good observation. I’ve left a note in the tracking issue for offset_of! on enums, because I couldn’t find any mention of uninhabited types around the whole topic, or any of the documentation.

There is, in one corner case: ?Sized generics must always be the tail field, and must always be laid out last in memory, to support unsizing coercions from &Struct<T> to &Struct<dyn Trait>. AIUI layout for sized T is functionality identical to the non-generic equivalent (but this is not guaranteed, of course).

But macros handling in-place construction might not be using generics for the uninhabited type, it might be e.g. a derive on a non-generic type.

Also, tuples as used in the OP are a generic type.

[src/main.rs:4:5] offset_of!((u8, i32), 1) = 4
[src/main.rs:5:5] offset_of!((u8, i32, ()), 1) = 0
1 Like

I think that particular example is more important to that discussion, as MaybeUninit<S<!>> is not a product type, but a sum type - MaybeUninit is a union. Or we may consider optimizing enums but not unions?

MaybeUninit<T> is guaranteed to have the same layout as T for all T, so MaybeUninit<S<!>> must have the same layout as S<!>.

3 Likes

Yes, exactly. Unions can’t be optimized, as too many details are already stably guaranteed. (For more than 5 years already) you don’t even need unsafe to partially initialize something in a union

union Foo {
    unit: (),
    other: (u8, Infallible),
}

fn main() {
    let mut x = Foo { unit: () };
    x.other.0 = 42;
}
1 Like