Option<T> and Result<T, E> in Depth

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>).