🎓 A1Learner
🔍

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 yet

Write 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 empty

Example

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?