I am wondering why &Void is not considered an empty type, when Void is, as, IIRC, p: &Voidmust point to a valid value of type Void, which there cannot be.
Making this would simplify code where you pattern match on a reference of an enum of which some branches are void.
EDIT: Apparently the question was not clear enough. An empty type in Rust has nothing to do with the void type in C. I am not speaking about ZST (zero-sized types), such as (). I am speaking of types such as Infallible and !, which are defined as follows
enum Infallible {}
Being an empty type is something that propagates in Rust, for instance, consider the following
enum Void {}
struct Empty {
foo: String,
bar: Vec<u32>,
boz: Void,
}
fn absurd<T>(e: Empty) -> T {
match e {}
}
this compiles because Rust is smart enough that no value of type Empty can be created, because that would entail that there would be a value of type Void, which there isn't.
The point of the question is: why this doesn't propagate to references too.
in rust pointers and references to zero sized structs still work like pointers and references to all other structs for the most part so &() is still a referernces and as such is nonempty,
this should not be that limiting for you tho since any address is valid for a zero sized struct
This has nothing to do with the void type in C, which is, retrospectively speaking, very ill-named. What I mean by Void is
enum Void {}
which is called an empty type Rust. Rust has two such types (that I know of) in the standard library, Infallible and ! ("never").
Rust has some kind of matching niceties related to empty type, meaning that if Rust is able to determine that the type of a value bound in a pattern in empty, you actually don't have to write off that branch.
Yes, but this has nothing to do with zero-sized types. Zero-sized types are types which have exactly one inhabitant, and, as such, require 0 bits to represent (hence the name). Empty types are types which have no inhabitant, and, as such, the question of representing such types is not even asked. You cannot have a value of type &Void, just as much as you cannot have a value of type Void. Again, this has nothing to do with the (ill-named) void type in C.
No, in Rust they are usually called "uninhabited types" (e.g. see here).
So your question is: why reference to an uninhabited type is not an uninhabited type itself. I don't have a clear answer here, but I think it's because it's just easier and more consistent to handle references in such way. But I could imagine a version of the language where it would've been different.
I would say that it's simply because it wasn't a priority to resolve all the issues related on uninhabited types.
We still don't have ! as full-blown types because there are too many unresolved small warts.
That reference to uninhabited type is not uninhabited is probably one of these wartsβ¦ and, as always: it's not hard to fix these warts β if you are Ok with breaking code that relied on them being present.
Keeping things backward compatible and still resolve these issues is hard, on the other hand.
I believe the reason &Infallible is not uninhabited is because the consensus seems to be leaning towards[1][2] the pointee of a reference not needing to be initialized to avoid UB. If this is indeed what we specify in the end, then constructing an &Infallible is actually allowed without UB, only later dereferencing said value would cause UB. This is similar to how constructing an &str that is not valid UTF-8 is not UB anymore, but passing this &str to any function that does not explicitly document it doesn't require UTF-8 is UB.
AFAIK, "uninhabited type" is a type theory lingo for what Rust internally calls empty types, so that's why it sometimes happen in some places. For instance, the RFC about the never type explicitely describes what an empty type is, and what are properties of empty types.
Both terms are used in Rust. Part of the compiler error for something like match e where e is a reference to an empty/uninhabited type is "references are always considered inhabited" (emphasis added). Personally on this forum I've seen "uninhabited" more often, so I'd personally stick with that. "Empty" is fine too, but it may cause you to explain what that means even though I personally think it's pretty "obvious".
As far as zero-sized types (ZSTs) are concerned, that is a related but orthogonal concept from the cardinality of the type. Many uninhabited/empty types and unit types have 0 size (i.e., are ZSTs), but there are some that aren't:
#![feature(never_type)]
#![expect(dead_code, reason = "example")]
enum Empty {}
#[repr(u8)]
enum Unit {
A,
}
enum ZstAndUnit {
A,
}
struct AnotherZstAndUnit;
fn main() {
// Empty/uninhabited types that are also ZSTs:
assert_eq!(0, size_of::<!>());
assert_eq!(0, size_of::<Empty>());
// Unit types that are also ZSTs:
assert_eq!(0, size_of::<()>());
assert_eq!(0, size_of::<ZstAndUnit>());
assert_eq!(0, size_of::<AnotherZstAndUnit>());
// Empty/uninhabited types that are not ZSTs:
assert_eq!(1, size_of::<(bool, !)>());
assert_eq!(4, size_of::<(Empty, u32)>());
// Unit types that are not ZSTs:
assert_eq!(1, size_of::<Unit>());
}
None of that really explains why references are always non-empty/inhabited. Interestingly, as one of the links above shows, Miri and MiniRust do consider types like &! empty/uninhabited:
This then in turn lets us declare &! itself uninhabited (not sure if rustc actually does that), and &&!, and so on.
I'm somewhat confused by the fact that uninhabited types are ZSTs. In my mind, they should be more like -βST. Like, if you state that uninhabited types require -β bytes, then you can still, for instance, add size of elements of a structure (although that's not exactly how structure size works...), like
struct Foo {
bar: String,
baz: !,
}
has "size = 3 (String) + -β (!) = -β", which is coherent with the fact that it is uninhabited. This works coherently with many other observations you can do "theoretically" about memory (ie without considering alignment and that kind of stuff).
I guess they are considered ZST by the Rust compiler out of simplicity, to avoid carving out yet another special case.
+/-β isn't really a "number" though unless you are dealing with something like cardinal arithmetic. We want core::mem::size_of to return a usize and we want types like ! to be Sized, so returning -β isn't really possible.
You wouldn't be the first person perplexed by this. Here is a response from a different post that states exactly what you did:
Ideally it should be -β:
2-β = 0, log 0 = -β
size of (u32, Never) would be 4 + (-β) = -β
Yes it probably wouldn't be practical to have an enum like
enum NumOrMInf {
MInf,
Num(usize),
}
This would work normally, as MaybeUninit<!> is a union of () and !, and the size of a union is the max of the sizes, and max(0, -β) = 0.
Somehow, this always makes sense because if you have a value of type !, then this execution branch is not happening, so you can assume you have infinite memory.
You are missing a key part of MaybeUninit<T>: MaybeUninit<T> is guaranteed to have the same size, alignment, and ABI as T. MaybeUninit is very much a special kind of union.
To expand on this, having uninhabited types of size -β would create a contradiction:
MaybeUninit<!> must have the same layout as !, so it must be size -β, and make containers of it size -β too, so containers of it can only be be uninhabited.
MaybeUninit<T> must be always inhabited (by at least MaybeUninit::uninit()), so MaybeUninit<!> is always inhabited, so MaybeUninit<!> must not make containers of it uninhabited.
It might be possible to solve this problem by having a trait bound expressing one or the other property rather than having it be a property that everyMaybeUninit offers, but that would need its own additional design.
Actually, isn't the definition of union size the maximum of the component sizes? Therefore that's actually fine, if uninit is of size 0.
Edit, ah right, it's the explicit "same layout" requirement, not the implied same layout as being a union with a ZST.
I guess you could fix that with relaxing the meaning of "same layout" for an uninhabited type such that everything "has the same layout" as one (probably actually changing the text to "compatible layout"), but that seems like a scary thread to pull....