Pre-Pre-RFC: Nonconstructible/opaque ZSTs


While browsing the source for RefCell the other day, I noticed the following definition:

pub struct BorrowError {
    _private: (),

This type is returned when we try to borrow a RefCell's contents with try_borrow and friends. It would be nice if we could declare this as pub struct BorrowError;, so that instances could be created without initializing the dummy field. However, this declaration makes BorrowError publicly constructible by struct literal syntax, so adding fields is a breaking change that requires a major version bump.

I would go so far as to call this feature a footgun, since developers might not realize that by writing struct Zst;, which is the most natural way to define such a struct, they’ve committed to a layout of the type unintentionally.

Possible solutions:

  • Mention this pattern in the Book. A quick glance reveals that there isn’t an explanation of this problem. (Though I don’t know that the book talks about semantic versioning at all? I could be wrong on all counts.)
  • Make pub struct Zst; not allow for construction outside of the current module, and make it opt-in with pub struct Zst(pub);. This is a breaking change, though churn should be minor since I don’t expect this to be a commonly used feature. I’m not sure what the best lint-then-remove path is, though…
  • Reverse the above, with an opt-out mechanism: pub struct Zst(priv);. While this avoid breakage, I really don’t think the use of the priv keyword is appropriate unless it’s adopted in other places, like enum variants.


There is a significantly more compact form that already works today:

pub struct BorrowError(());

The BorrowError type is probably defined the way it is because it was created back when tuple struct fields were always public.


Oh yeah, I originally included that in a draft of the post but deleted it at one point… thanks!

The main point I’m making is that I don’t think it’s obvious whether pub struct Zst; can change its layout or not; for all other structs, changing layout implies breakage only when the struct has public fields, i.e. pub in its body. I think this asymmetry is confusing at best and a footgun at worst.


If one is good with a crate-level restriction, there’s always

pub struct Zst;

Using the normal “the layout can change” feature.