Rust Uninitialized Memory
MaybeUninit is a wrapper that holds a value that may not exist yet. Code that fills a buffer piece by piece sometimes needs memory that starts empty.
Memory that starts empty
MaybeUninit<T> wraps a T that may not exist yet. Creating one is always safe, because it promises nothing:
let slot = MaybeUninit::<i32>::uninit(); // empty: no value inside yetWrite a value in
Fill the slot with write. Dropping a MaybeUninit does nothing, so writing never drops whatever garbage was there:
let mut slot = MaybeUninit::<i32>::uninit();
slot.write(42);Take the value out
Once the slot is filled, assume_init takes the value out. It is unsafe: call it on a still-empty slot and the "value" is undefined behaviour.
slot.write(42);
let answer = unsafe { slot.assume_init() };
println!("answer = {answer}");Build an array one slot at a time
The classic use: an array of empty slots. It is legal where a half-built array is not. Fill each slot. Then take the values out:
let mut slots: [MaybeUninit<i32>; 2] =
[MaybeUninit::uninit(), MaybeUninit::uninit()];
slots[0].write(7); slots[1].write(8);Work through the pointer until the value exists
While a slot is still empty, there is no value to borrow. Go through the raw pointer from as_mut_ptr() instead:
let mut slot = MaybeUninit::<i32>::uninit();
let ptr = slot.as_mut_ptr(); // no &i32 yet — the slot is still emptyExample
use std::mem::MaybeUninit;
fn main() {
let mut slots: [MaybeUninit<String>; 3] = std::array::from_fn(|_| MaybeUninit::uninit());
slots[0].write("one".to_string());
slots[1].write("two".to_string());
slots[2].write("three".to_string());
// SAFETY: all three slots were written above, so each one is initialised.
let words: Vec<String> = slots
.into_iter()
.map(|slot| unsafe { slot.assume_init() })
.collect();
println!("words = {:?}", words);
}Key points
- Dropping a MaybeUninit does nothing: if the value inside needs cleanup, that cleanup is your job
- Creating a &T or &mut T to still-empty memory is already undefined behaviour.
- Work through the raw pointer from as_mut_ptr() instead.
- Do so until the value really exists.
- An older shortcut, mem::uninitialized(), is deprecated — it pretended empty memory was already a value.
- Safe Rust never lets code read a variable before it holds a value.
- Hardware code can also need memory that starts empty.
- MaybeUninit holds memory for a value that may not exist yet.
In this example, slots starts as three empty MaybeUninit<String> wrappers. Safe Rust could never hold a half-built array of three Strings. But it can hold this. Each slot is then filled with write, one String at a time. Only after all three writes does the code take the values out. into_iter walks the slots. assume_init turns each wrapper into the String it now certainly holds. It sits inside unsafe, with the SAFETY reason written above. The collected words print as ["one", "two", "three"]. Call assume_init one line earlier, before a write. The program would then be reading garbage as a String. That is undefined behaviour.
Note
MaybeUninit makes the empty state a type you can see, instead of a trap.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
What happens when a MaybeUninit<String> that holds a String is dropped?