Option<T> Representation
In memory, Option<T> is represented either as a tagged union (a discriminant tag plus space for T and alignment padding) or—when T has an invalid bit pattern (niche), such as 0 for references and non-null pointers—as a niche-optimized value occupying the exact same size as T (size_of::<Option<T>>() == size_of::<T>()).
Standard Tagged Layout vs. Niche-Optimized Layout
When T can inhabit every possible bit pattern of its size (such as u32 or i64), the compiler must store a separate 1-byte discriminant tag (0 for None, 1 for Some) plus padding to match align_of::<T>():
1. Tagged Union Layout: `Option<u64>` (16 bytes on 64-bit, align = 8)
+---------+-------------------+----------------------------------+
| tag: u8 | padding (7 bytes) | payload: u64 (8 bytes) |
+---------+-------------------+----------------------------------+
2. Niche-Optimized Layout: `Option<&u64>` or `Option<Box<u64>>` (8 bytes on 64-bit!)
+----------------------------------------------------------------+
| 0x0000_0000_0000_0000 ──> None |
| Any non-zero address ──> Some(ptr) |
+----------------------------------------------------------------+
Types That Guarantee the Null-Pointer / Niche Optimization
Because references (&T, &mut T), Box<T>, Rc<T>, Arc<T>, fn pointers, and NonNull<T> can never be null (0), and NonZero<u32> can never be 0, the compiler uses the unused bit pattern 0 (or another invalid bit pattern for bool, char, and enums) to represent Option::None with zero space overhead:
Inner Type T |
size_of::<T>() |
size_of::<Option<T>>() |
Niche Used for None |
|---|---|---|---|
u64 |
8 bytes |
16 bytes (8 B tag/pad + 8 B u64) |
None (all $2^{64}$ bit patterns valid) |
std::num::NonZeroU64 |
8 bytes |
8 bytes |
0 |
&T, &mut T, Box<T> (where T: Sized) |
8 bytes |
8 bytes |
Null pointer (0x0) |
&str, &[T], Box<dyn Trait> |
16 bytes |
16 bytes |
Null data pointer (0x0) |
String, Vec<T> |
24 bytes |
24 bytes |
Null internal NonNull<u8> pointer |
bool |
1 byte |
1 byte |
Value 2 (0 = false, 1 = true) |
char |
4 bytes |
4 bytes |
Code point $> \text{0x10FFFF}$ |
use std::mem::size_of;
use std::num::NonZeroU64;
fn main() {
println!("u64 vs Option<u64>: {} vs {} bytes", size_of::<u64>(), size_of::<Option<u64>>());
println!("NonZeroU64 vs Option<NonZeroU64>: {} vs {} bytes", size_of::<NonZeroU64>(), size_of::<Option<NonZeroU64>>());
println!("&u64 vs Option<&u64>: {} vs {} bytes", size_of::<&u64>(), size_of::<Option<&u64>>());
println!("Box<u64> vs Option<Box<u64>>: {} vs {} bytes", size_of::<Box<u64>>(), size_of::<Option<Box<u64>>());
println!("String vs Option<String>: {} vs {} bytes", size_of::<String>(), size_of::<Option<String>>());
println!("bool vs Option<bool>: {} vs {} bytes", size_of::<bool>(), size_of::<Option<bool>>());
}
u64 vs Option<u64>: 8 vs 16 bytes
NonZeroU64 vs Option<NonZeroU64>: 8 vs 8 bytes
&u64 vs Option<&u64>: 8 vs 8 bytes
Box<u64> vs Option<Box<u64>>: 8 vs 8 bytes
String vs Option<String>: 24 vs 24 bytes
bool vs Option<bool>: 1 vs 1 bytes
Converting &Option<T> to Option<&T> with .as_ref() and .as_mut()
When a struct stores an Option<String> field and a method only borrows &self, you cannot move the String out of self.field directly. Calling self.field.as_ref() converts &Option<T> into Option<&T> (or .as_deref() to convert &Option<String> directly into Option<&str>).