Introduction
Uninitialized memory is one of the most dangerous areas of unsafe Rust. Reading an uninitialized value is always undefined behavior — even for types like i32 that have no invalid bit patterns. MaybeUninit<T> is the safe way to work with memory that might not yet contain a valid value.
Key Concepts
MaybeUninit<T>: A union type that tells the compiler "this memory may or may not contain a validT." It suppresses the validity requirement until you callassume_init().assume_init(): Asserts that theMaybeUninithas been initialized and extracts the value. Calling this on uninitialized memory is UB.write(): Writes a value into theMaybeUninit, initializing it.- Validity invariant: The rule that every value of a type must be one of the type's valid bit patterns.
Real World Context
The standard library uses MaybeUninit internally for Vec, HashMap, and BTreeMap to initialize arrays element by element. Performance-sensitive code uses it to avoid default-initializing large buffers. FFI code uses it for C-style "out parameters."
Deep Dive
The Problem: Uninitialized Memory is Always UB
Even for simple types, reading uninitialized memory is undefined behavior:
rust// UB! Even though i32 has no invalid bit patterns let x: i32; println!("{x}"); // Compiler error in safe code, UB if you bypass it // UB! The deprecated way let x: bool = unsafe { std::mem::uninitialized() }; // NEVER DO THIS // A bool must be 0 or 1; random bits could be 0x42, which is invalid
The old std::mem::uninitialized() function is deprecated because it creates instant UB for many types.
MaybeUninit: The Correct Approach
MaybeUninit lets you safely represent uninitialized memory:
rustuse std::mem::MaybeUninit; // Create uninitialized storage let mut slot: MaybeUninit<i32> = MaybeUninit::uninit(); // Write a value (initializes the memory) slot.write(42); // Now it's safe to read let value = unsafe { slot.assume_init() }; assert_eq!(value, 42);
The key insight: MaybeUninit::uninit() is safe because it does not claim the memory contains a valid value. Only assume_init() requires unsafe, because that's where you assert initialization has happened.
Initializing Arrays Element by Element
A common use case is building a large array without default values:
rustuse std::mem::MaybeUninit; fn create_squares() -> [i32; 100] { let mut arr: [MaybeUninit<i32>; 100] = [const { MaybeUninit::uninit() }; 100]; for i in 0..100 { arr[i].write((i * i) as i32); } // SAFETY: All 100 elements were initialized in the loop unsafe { let ptr = arr.as_ptr() as *const [i32; 100]; std::ptr::read(ptr) } }
This avoids initializing the array to zero first and then overwriting every element.
Out Parameter Pattern for FFI
C APIs often use out parameters. MaybeUninit is perfect for this:
rustunsafe extern "C" { fn get_system_info(out: *mut SystemInfo) -> libc::c_int; } fn fetch_info() -> Option<SystemInfo> { let mut info = MaybeUninit::<SystemInfo>::uninit(); // SAFETY: get_system_info writes to the pointer on success let result = unsafe { get_system_info(info.as_mut_ptr()) }; if result == 0 { // SAFETY: result == 0 means the C function initialized the struct Some(unsafe { info.assume_init() }) } else { None } }
This avoids constructing a dummy value just to have it overwritten.
Never Use mem::zeroed() Carelessly
Zeroing memory is only safe for types where all-zeros is valid:
rust// SAFE: all-zeros is a valid i32 let n: i32 = unsafe { std::mem::zeroed() }; // UB! String expects a valid heap pointer, not null let s: String = unsafe { std::mem::zeroed() }; // UB! References can never be null let r: &i32 = unsafe { std::mem::zeroed() };
When in doubt, use MaybeUninit instead of zeroed().
Common Pitfalls
- Calling
assume_init()before initialization — This is UB, even for integer types. The compiler may optimize based on the assumption that initialized values are valid. - Partial initialization of arrays — If you initialize only some elements and then call
assume_init(), the uninitialized elements are UB. Use a counter to track how many elements have been initialized. - Using
mem::zeroed()for types with validity invariants — References,NonNull,bool, andStringall have bit patterns that zero does not satisfy.
Best Practices
- Use
MaybeUninitinstead ofmem::uninitialized— The old function is deprecated and almost always UB.MaybeUninitis the correct replacement. - Track initialization state — When initializing arrays or buffers incrementally, use a length counter so you know which elements are initialized.
- Prefer
write()overas_mut_ptr()—write()is clearer about intent and handles dropping the previous value if needed.
Summary
MaybeUninit<T>safely represents potentially uninitialized memory.assume_init()is the unsafe assertion that initialization has occurred.- Use
write()to initialize aMaybeUninitslot. - Never use
mem::uninitialized()— it is deprecated and almost always UB. mem::zeroed()is only safe for types where all-zeros is a valid value.
Code Examples
use std::mem::MaybeUninit;
/// Initialize an array using a closure, element by element.
fn init_array<T, F, const N: usize>(mut f: F) -> [T; N]
where
F: FnMut(usize) -> T,
{
let mut arr: [MaybeUninit<T>; N] =
[const { MaybeUninit::uninit() }; N];
for (i, elem) in arr.iter_mut().enumerate() {
elem.write(f(i));
}
// SAFETY: All N elements were initialized in the loop above
unsafe {
let ptr = arr.as_ptr() as *const [T; N];
std::ptr::read(ptr)
}
}
// Usage: create an array of squares
let squares: [i32; 10] = init_array(|i| (i * i) as i32);
assert_eq!(squares, [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]);
// Usage: create an array of formatted strings
let labels: [String; 5] = init_array(|i| format!("item_{i}"));
assert_eq!(labels[2], "item_2");