Usage of the never type as field of a struct

Hey folks,

in this code I've found in the ForLt struct the never type as a field. I wonder why it's there. My assumption is to prevent construction of values of the struct. Might this be correct? :face_with_monocle:

Regards keks

Almost surely so, given the comments.

2 Likes

Thx! :slight_smile: I didn't know this kind of pattern. That's actually a pretty elegant way to do this. :astonished:

An empty enum is also uninhabited (can't be constructed, like !), so you can use that yourself on stable. Some people prefer that over ZSTs for "marker structs" that are just used to parameterize traits or generic structs, etc.

(I didn't actually look into what that crate does.)

2 Likes

I had a quick look at what the higher-kinded-types crate does. It looks very clever.

I recognise @Yandros profile pic from around here :slight_smile: and that gives me some confidence that it is, in fact, very clever!

2 Likes

This. For a marker type that is only intended to be used in the type reälm, doing:

enum MyMarkerType {}

offers the right "minimal" API w.r.t. runtime values, nobody can possibly have these around.

But then, I had to carry a generic <T>, which required a PhantomData of some sorts, so I settled for slapping an uninhabited type alongside it, and since I already had the canonical one, !, around, I settled for using it rather than a new empty enum.

- enum Foo<T> {}
+ struct Foo<T>(PhantomData<...>, !);

would summarize the gut feeling I had when writing this.

But in practice the extra ! does not play any role whatsoever, and I did not think too much about people looking at this code with that much scrutiny :sweat_smile:, otherwise I'd probably have gone for the simpler approach of just having the PhantomData.


Now that we are on this topic, do note that this pattern can be genuinely useful elsewhere: as PhantomVariant<T>:

type Never = !; /* or:
enum Never {} // */

type PhantomVariant<T> = (PhantomData<T>, Never);

which lets you "put a PhantomData<T> inside an enum":

  enum Enum<T> {
      Foo,
      Bar,
+     _Phantom(PhantomVariant<T>),
  }

Without the uninhabited type alongside the PhantomData, the whole variant would not be optimized out at runtime, which in turn could hinder stuff such as discriminant elision.

5 Likes

Hmm... but for example here using the mentioned PhantomVariant<T> with the never type or PhantomData<T> seems ot make no difference. :thinking:

edit: When I remove the None variant in your example it also makes no difference (example 1) and when I add more variants it's the same (example 2). That's pretty confusing. :sweat_smile:

Sorry, I should have used could rather than would in my sentence: I've now edited it accordingly.

A Variant(PhantomData<…>) as in:

  • enum Enum {
        …
        Variant(PhantomData<…>),
    }
    

will have the same impact on Enum's discriminant elision —and thus size reduction— (or lack thereof) as:

  • enum Enum {
        …
        Variant, // <- unit variant!
    }
    

For an enum with discriminant elision, and at least one niche (forbidden bit-pattern), that Enum::Variant can be encoded using this bit-pattern, thereby maintaining the discriminant elision:

enum Enum {
    Foo = 0b0000_0000,
    Bar = 0b0000_0001,
    /* 0b0000_0010 is available! */

    Variant, // <- can use `0b0000_0010`
}
  • Note: while a nice optimization, in the general case it is not something Rust guarantees, for the sake of future-proofing compiler evolution.

But for an enum with no niches left among its variants, such as:

enum MyOption {
    Some(::core::ptr::NonNull<()>), // <- `!0b0000_…_0000`
    None, // <- `0b0000_…_0000` available!
    /* No more room in these bits */

    Variant, // Uh-oh, we now need an extra/companion byte to
             // encode `matches!(Variant)`: the discriminant
             // is no longer elided!
}

Which is what happens with:

enum MyOption<T> {
    Some(::core::ptr::NonNull<()>),
    None,

    _Phantom(PhantomData<T>),
}

but not with:

enum MyOption<T> {
    Some(::core::ptr::NonNull<()>),
    None,

    _Phantom(PhantomVariant<T>),
}

This can happen because PhantomVariant<T>, by virtue of being uninhabited (w.r.t. safe-code) and zero-sized (w.r.t. convoluted unsafe shenanigans with MaybeUninit and structurally projecting it), is known by the compiler to be unobservable; so there is no need to encode its discriminant anywhere (since it can never happen).

3 Likes

Thank you for your explanation, sir! :slight_smile:

@other readers
For some background about the terms @Yandros is using in his post look here. :notebook_with_decorative_cover:

2 Likes