Enum Layout
Enum layout defines how a Rust enum (tagged union) is represented in memory. An enum instance stores an active discriminant (tag) that identifies which variant is currently held, alongside space large enough to hold the payload of its largest variant.
General Tagged-Union Layout
When no niche optimization applies, an enum is laid out as a discriminant tag plus a union of all variant payloads, padded to the maximum alignment required by either the tag or any variant field:
$$\text{size}(\text{Enum}) = \text{round_up}\big(\text{size}(\text{Tag}) + \text{padding} + \max_i \text{size}(\text{Variant}_i),; \max_i \text{align}(\text{Variant}_i)\big)$$
enum Message {
Quit, // payload: 0 bytes
Move { x: i32, y: i32 }, // payload: 8 bytes, align 4
Write(u64, u64), // payload: 16 bytes, align 8
}
Memory Layout of `Message` (24 bytes total, align 8):
Variant `Quit`:
+----------+-----------------------+-------------------------------+
| tag = 0 | padding (7 bytes) | unused payload (16 bytes) |
+----------+-----------------------+-------------------------------+
0 1..7 8..23
Variant `Move { x: 10, y: 20 }`:
+----------+-----------+-----------+-----------+-------------------+
| tag = 1 | pad (3 B) | x: i32(4B)| y: i32(4B)| unused (8 bytes) |
+----------+-----------+-----------+-----------+-------------------+
0 1..3 4..7 8..11 12..23
Variant `Write(1, 2)`:
+----------+-----------------------+---------------+---------------+
| tag = 2 | padding (7 bytes) | .0: u64 (8B) | .1: u64 (8B) |
+----------+-----------------------+---------------+---------------+
0 1..7 8..15 16..23
Notice that under #[repr(Rust)], a smaller variant such as Move can place one of its i32 fields inside the padding gap between the 1-byte tag and the 8-byte aligned payload offset, or keep all variants aligned to 8 bytes.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(u64, u64),
}
fn main() {
println!("size of Message: {} bytes", size_of::<Message>());
println!("align of Message: {} bytes", align_of::<Message>());
}
size of Message: 24 bytes
align of Message: 8 bytes
Categories of Enum Layouts
| Enum Shape | Example | Memory Representation | Size (64-bit) |
|---|---|---|---|
| Empty (uninhabited) | enum Never {} |
Cannot be instantiated | 0 bytes |
| Single-variant (unit) | enum One { Only } |
Zero-sized type (no tag needed) | 0 bytes |
| Single-variant (with data) | enum Wrap { Data(u64) } |
Identical to struct Wrap(u64) (no tag needed) |
8 bytes |
| Fieldless (C-like) | enum Color { R, G, B } |
Smallest integer holding all variant tags (u8 for $\le 256$ variants) |
1 byte |
| Niche-optimized | Option<&u64> |
Tag folded into invalid bit pattern (0x0) of inner payload |
8 bytes |
| General payload enum | Result<u64, u64> |
Explicit tag + aligned union of largest variant | 16 bytes |
Mitigating Large Variant Bloat with Box<T>
Because every instance of an enum is sized to accommodate its largest variant, a single oversized variant inflates the stack footprint of all instances. Wrapping the large variant's payload in Box<T> shrinks the variant payload to a single pointer (8 bytes on 64-bit targets).
enum UnboxedEvent {
Heartbeat,
Snapshot([u8; 1024]),
}
enum BoxedEvent {
Heartbeat,
Snapshot(Box<[u8; 1024]>),
}
fn main() {
println!("UnboxedEvent size: {} bytes", size_of::<UnboxedEvent>());
println!("BoxedEvent size: {} bytes", size_of::<BoxedEvent>());
}
UnboxedEvent size: 1025 bytes
BoxedEvent size: 16 bytes