Memory Layout and Representation

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