C has bitfields. Bitfields are great, because writing shifts and other bit operations is a pain and error-prone. There's plenty of fancy macros for emulating bitfields, sure, but it's limited one specific kind of packing within a struct.
I'm interested in how far we can take cramming down fields in a struct on the assumption that you can't take references to them. For example, consider the classic, extremely wasteful struct:
struct Foo {
a: Option<u32>,
b: Option<u32>,
c: Option<u32>,
}
A whole 93 bits of this struct are wasted, because every one of those options expects to be able to be the referent of a reference, and thus need to have identical layouts, so the presence bit gets rounded up to 1. However, if we aggregated all the presence bits into a byte, we save 64 bits! However, this means that you can't get a reference to those Options, because they're discontinuous!
You see this kind of optimization a lot, especially in code that needs to really pack its representations for efficiency, like a regular expressions engine or generated protobuf code. It would certainly be nice to have compiler support, similar to niche-finding, for saying "hey, I'm never going to need a reference to this field, can you please lay it out in some crazy way?
Here's a strawman to illustrate what I want. Suppose we add a #[flat] annotation that you can put on a field:
struct Foo(u8, u16);
struct Bar(#[flat] Foo, u8);
Under normal circumstances, Bar is going to take up three u16s, because Foo needs to be padded out, even if Rust decided to order the u8 after the u16. However, because we've requested that Foo be flattened, the definition is instead treated as if it were
struct ActualBar(u8, u16, u8);
We save two bytes, at the cost that &bar.0 is now a hard error. The field bar.0 instead has Cell semantics. However, &bar.0.1 is valid, because the u16 will be aligned, as would be the case in ActualBar.
The local struct version is easy, but it gets a bit messier when you involve pre-existing types. We want
struct Foo {
#[flat]
a: Option<u32>,
#[flat]
b: Option<u32>,
#[flat]
c: Option<u32>,
}
to be laid out as (u32, u32, u32, u8, padding) or similar. But suddenly, because you can't take references, you can't do simple things like foo.a.is_some(), because that requires a reference to a. For now, I think it's best to just assume you can only flatten Copy types, but it's probably possible to extend to non-Copy types in some cases; the Copy case is probably the most interesting one. You should also be able to write
match &foo.a {
Some(a) => a, // types at &u32
None => ...,
x => /* not allowed! */,
}
but I'm not sure how to best specify this in a way that doesn't feel unnecessarily magical...
Open questions from my perspective:
- Should "flatness" be a property of a field, or of a type? For fields, it means that you can make arbitrary existing types flat without incurring the no-references-allowed penalty. For a type, it means we can make it a trait, which could be useful in generic contexts, but it's not clear to me how.
- How do we deal with matching on a flat enum field to extract references into it?
.