From 13a9e799bbc6e31e1075f8b9223780e7f79a72ae Mon Sep 17 00:00:00 2001 From: scottmcm Date: Fri, 15 Aug 2025 07:48:37 +0000 Subject: [PATCH 1/3] Add a note about uninhabited-struct layout optimization --- src/frequently-requested-changes.md | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/frequently-requested-changes.md b/src/frequently-requested-changes.md index 3e5229b..75e608b 100644 --- a/src/frequently-requested-changes.md +++ b/src/frequently-requested-changes.md @@ -217,3 +217,47 @@ Cross-referencing to other discussions: * https://github.com/rust-lang/rfcs/issues/1397 * https://github.com/rust-lang/rust/issues/17027 * https://github.com/rust-lang/unsafe-code-guidelines/issues/176 + +## Uninhabited `struct`s should all be ZSTs + +It makes conceptual sense that if something is uninhabited, it shouldn't take up any space. +In safe code that works great, but we tried it and ran into problems, so it's not likely to happen. + +The biggest problem is related to field projection during initialization. Take this code: + +```rust +pub fn make_pair(a0: impl Fn() -> T0, a1: impl Fn() -> T1) -> Box<(T0, T1)> { + let mut mu = Box::<(T0, T1)>::new_uninit(); + unsafe { + let p0 = &raw mut (*mu.as_mut_ptr()).0; + p0.write(a0()); + + let p1 = &raw mut (*mu.as_mut_ptr()).1; + p1.write(a1()); + + mu.assume_init() + } +} +``` + +Is that *sound*? It sure looks reasonable -- after all, it initialized both the fields -- but +it depends on exactly what the layout rules are. + +(Aside: Note that a production-ready version of that function should also handle unwinding cleanup +of the first value if constructing the second panicked, but for simplicity of presentation we're +ignoring that part here because leaking is still *sound*.) + +For something simple like `make_pair::`, it's clearly fine. But with `make_pair::` +it's *only* sound if we *don't* let `(u32, !)` become a ZST. We need the allocation for the box +to be large enough to write that `u32` without being an obviously-UB out-of-bounds write. + +Thus if we wanted to always have uninhabited product types be ZSTs, we'd need to give up on certain +other rules, perhaps the one that `T` and `MaybeUninit` always have the same size. So far, the +simpler, less-error-prone experience for writing unsafe code has won out over the minimal space +savings possible from shrinking the types. After all, while it's not necessarily fully unreachable, +as something like `make_pair(|| a, || loop { … })` would still need to allocate the space despite +that never reaching the `assume_init` part, it's still unlikely that this occurs frequently. + +There *is* still interest in doing optimizations like this on *sum* types, however. There's more +to potentially be gained there since one variant of a `union` or `enum` being uninhabited doesn't +keep the whole *value* from being uninhabited the way an uninhabited field does in a `struct`. From c01fff94b29fedce2280d19d5231dab56468e89f Mon Sep 17 00:00:00 2001 From: scottmcm Date: Fri, 15 Aug 2025 18:01:45 +0000 Subject: [PATCH 2/3] Update src/frequently-requested-changes.md Co-authored-by: Jacob Lifshay --- src/frequently-requested-changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frequently-requested-changes.md b/src/frequently-requested-changes.md index 75e608b..58f28ee 100644 --- a/src/frequently-requested-changes.md +++ b/src/frequently-requested-changes.md @@ -258,6 +258,6 @@ savings possible from shrinking the types. After all, while it's not necessaril as something like `make_pair(|| a, || loop { … })` would still need to allocate the space despite that never reaching the `assume_init` part, it's still unlikely that this occurs frequently. -There *is* still interest in doing optimizations like this on *sum* types, however. There's more +There *is* still interest in maybe doing optimizations like this on *sum* types, however. There's more to potentially be gained there since one variant of a `union` or `enum` being uninhabited doesn't keep the whole *value* from being uninhabited the way an uninhabited field does in a `struct`. From 0c9d7a84ea1cf20d778d6c205ec5d97214023e66 Mon Sep 17 00:00:00 2001 From: scottmcm Date: Fri, 15 Aug 2025 18:58:20 +0000 Subject: [PATCH 3/3] Remove the `union` mention --- src/frequently-requested-changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frequently-requested-changes.md b/src/frequently-requested-changes.md index 58f28ee..cb6f388 100644 --- a/src/frequently-requested-changes.md +++ b/src/frequently-requested-changes.md @@ -259,5 +259,5 @@ as something like `make_pair(|| a, || loop { … })` would still need to allocat that never reaching the `assume_init` part, it's still unlikely that this occurs frequently. There *is* still interest in maybe doing optimizations like this on *sum* types, however. There's more -to potentially be gained there since one variant of a `union` or `enum` being uninhabited doesn't +to potentially be gained there since one variant of an `enum` being uninhabited doesn't keep the whole *value* from being uninhabited the way an uninhabited field does in a `struct`.