Memory Layout and Representation

Niche Optimization

A niche is a bit pattern in a type's memory representation that is never valid for any instance of that type. Niche optimization (also called discriminant elision) allows the compiler to store an outer enum's variant tag inside a payload field's unused bit patterns, eliminating the need for a separate discriminant byte or word.

Null-Pointer Optimization (Option<&T>)

References (&T, &mut T), smart pointers (Box<T>, Rc<T>, Arc<T>), std::ptr::NonNull<T>, and function pointers (fn()) are guaranteed never to be null (0x0). The compiler encodes Option::None as the all-zero bit pattern 0x0, making Option<T> the exact same size as T.

Option<&u64> (8 bytes total on 64-bit targets, 0 extra tag bytes):

Some(ptr):
+-------------------------------------------------------------------+
| 0x0000_0001_6fdf_ee88 (non-zero valid memory address)             |
+-------------------------------------------------------------------+

None:
+-------------------------------------------------------------------+
| 0x0000_0000_0000_0000 (niche bit pattern: null address)           |
+-------------------------------------------------------------------+

Because Option<&T> and Option<extern "C" fn()> are guaranteed to use the null-pointer niche, they can be passed directly across C FFI boundaries where C expects a nullable pointer.

Built-in Types with Niches

Type Valid Bit Patterns Available Niche Bit Patterns size_of::<T>() vs size_of::<Option<T>>()
&T, &mut T, Box<T>, NonNull<T> Non-zero (1..=usize::MAX) 0 (1 niche) 8 vs 8
fn() Non-zero code address 0 (1 niche) 8 vs 8
NonZeroU8 .. NonZeroU128 1..=MAX 0 (1 niche) N vs N
bool 0 (false), 1 (true) 2..=255 (254 niches) 1 vs 1
char 0..=0xD7FF, 0xE000..=0x10FFFF Surrogates + > 0x10FFFF ($> 4.29 \times 10^9$ niches) 4 vs 4
Ordering (Less, Equal, Greater) -1, 0, 1 2..=127, -128..=-2 (253 niches) 1 vs 1
String, Vec<T> Contains non-null Unique<T> pointer 0 in pointer field (1 niche) 24 vs 24

Multi-Variant Niche Packing

When an inner type has multiple unused bit patterns (such as bool with 254 niches or custom fieldless enums), the compiler can pack dozens of outer unit variants into the same byte without increasing the type's size:

use std::num::NonZeroU32;

enum CompactState {
    Active(bool),
    Pending,
    Suspended,
    Terminated,
}

fn main() {
    println!("size of u32:                {} bytes", size_of::<u32>());
    println!("size of Option<u32>:        {} bytes", size_of::<Option<u32>>());
    println!("size of NonZeroU32:         {} bytes", size_of::<NonZeroU32>());
    println!("size of Option<NonZeroU32>: {} bytes", size_of::<Option<NonZeroU32>>());
    println!("size of CompactState:       {} bytes", size_of::<CompactState>());
    println!("size of Option<String>:     {} bytes", size_of::<Option<String>>());
}
size of u32:                4 bytes
size of Option<u32>:        8 bytes
size of NonZeroU32:         4 bytes
size of Option<NonZeroU32>: 4 bytes
size of CompactState:       1 bytes
size of Option<String>:     24 bytes

Nested Niche Propagation

Niches propagate transitively through structs and tuples. If a struct contains at least one field with a niche (for example, a NonZeroU64, a bool, or a NonNull<T>), any outer Option<MyStruct> or Result<MyStruct, ()> reuses that field's niche at zero space cost.

use std::num::NonZeroU16;

struct Connection {
    port: NonZeroU16,
    ip: [u8; 4],
}

fn main() {
    println!("size of Connection:         {} bytes", size_of::<Connection>());
    println!("size of Option<Connection>: {} bytes", size_of::<Option<Connection>>());
}
size of Connection:         6 bytes
size of Option<Connection>: 6 bytes