How do I work with MaybeUninit in Rust?
Walkthrough
MaybeUninit is a wrapper type for safely handling uninitialized memory in Rust. It replaces the deprecated mem::uninitialized() and mem::zeroed() functions, providing a type-safe way to work with memory that may or may not be initialized.
Key concepts:
- Uninitialized memory ā Memory with arbitrary, undefined contents
- Safe wrapper ā Prevents undefined behavior from reading uninitialized data
- Zero-cost ā Same size as
T, compiles to raw memory - Gradual initialization ā Build values field by field
Why MaybeUninit is necessary:
- Reading uninitialized memory is undefined behavior
mem::uninitialized()was unsound and deprecated- Some APIs require out parameters
- Performance-critical code may need to skip initialization
Safety invariants:
- Never read from
MaybeUninituntil fully initialized - Use
assume_init()only after initialization is complete - For arrays, use
assume_init_slice()orslice_assume_init_mut()
Code Examples
Basic MaybeUninit Usage
use std::mem::MaybeUninit;
fn main() {
// Create uninitialized memory
let mut uninit: MaybeUninit<i32> = MaybeUninit::uninit();
// Initialize it
uninit.write(42);
// Now safe to assume it's initialized
let value: i32 = unsafe { uninit.assume_init() };
println!("Value: {}", value);
}Uninitialized Array
use std::mem::MaybeUninit;
fn main() {
// Create an uninitialized array
let mut arr: [MaybeUninit<i32>; 5] = MaybeUninit::uninit_array();
// Initialize each element
for (i, elem) in arr.iter_mut().enumerate() {
elem.write((i + 1) as i32);
}
// Convert to initialized array
let arr: [i32; 5] = unsafe {
MaybeUninit::array_assume_init(arr)
};
println!("Array: {:?}", arr);
}Gradual Initialization
use std::mem::MaybeUninit;
struct Point {
x: f64,
y: f64,
}
fn main() {
let mut point = MaybeUninit::<Point>::uninit();
// Initialize field by field using raw pointers
unsafe {
let ptr = point.as_mut_ptr();
// Initialize each field
(*ptr).x = 3.14;
(*ptr).y = 2.71;
}
// Now fully initialized, safe to assume_init
let point: Point = unsafe { point.assume_init() };
println!("Point: ({}, {})", point.x, point.y);
}Out Parameter Pattern
use std::mem::MaybeUninit;
// Simulate a C function that writes to an out parameter
fn compute_value(output: &mut MaybeUninit<i32>) {
output.write(42);
}
fn main() {
let mut result = MaybeUninit::uninit();
// Pass to function that initializes it
compute_value(&mut result);
// Safe because compute_value initialized it
let value = unsafe { result.assume_init() };
println!("Result: {}", value);
}FFI with Out Parameters
use std::mem::MaybeUninit;
use std::ffi::c_int;
// C function signature: int get_value(int* out);
// Returns 0 on success, non-zero on error
fn get_value_ffi(output: &mut MaybeUninit<c_int>) -> Result<(), std::io::Error> {
// Simulate calling C function
// In real code: unsafe { get_value(output.as_mut_ptr()) }
// Simulate success
output.write(100);
Ok(())
}
fn main() {
let mut value = MaybeUninit::<c_int>::uninit();
match get_value_ffi(&mut value) {
Ok(()) => {
let v = unsafe { value.assume_init() };
println!("Got value: {}", v);
}
Err(e) => {
println!("Error: {}", e);
}
}
}Initializing Large Arrays
use std::mem::MaybeUninit;
fn main() {
// Large arrays are slow to initialize with [0; 1000000]
// But sometimes we need to fill each element anyway
const SIZE: usize = 100;
let mut arr: [MaybeUninit<String>; SIZE] = MaybeUninit::uninit_array();
// Initialize each element
for (i, elem) in arr.iter_mut().enumerate() {
elem.write(format!("Item {}", i));
}
// Convert to initialized array
let arr: [String; SIZE] = unsafe {
MaybeUninit::array_assume_init(arr)
};
println!("First: {}, Last: {}", arr[0], arr[SIZE - 1]);
}Uninitialized Buffer
use std::mem::MaybeUninit;
fn main() {
// Create an uninitialized buffer for reading
let mut buffer: [MaybeUninit<u8>; 1024] = MaybeUninit::uninit_array();
// Simulate reading data (only partially fills buffer)
let bytes_read: usize = 100; // Simulated
// In real code, you'd pass buffer.as_mut_ptr() to a read function
for i in 0..bytes_read {
buffer[i].write(b'X');
}
// Only the first `bytes_read` bytes are initialized
let initialized: &[u8] = unsafe {
MaybeUninit::slice_assume_init(&buffer[..bytes_read])
};
println!("Read {} bytes", initialized.len());
}Zeroed Memory
use std::mem::MaybeUninit;
fn main() {
// Sometimes you need zero-initialized memory
// Safe for types where all-zeros is a valid representation
let mut zeroed: MaybeUninit<i32> = MaybeUninit::zeroed();
let value = unsafe { zeroed.assume_init() };
println!("Zeroed i32: {}", value); // 0
// For types with references, zeroed is NOT safe!
// let bad: MaybeUninit<&i32> = MaybeUninit::zeroed();
// unsafe { bad.assume_init() } // UB! Null reference!
}MaybeUninit with Generic Types
use std::mem::MaybeUninit;
fn initialize_buffer<T>(size: usize, init_fn: impl Fn(usize) -> T) -> Vec<T> {
let mut buffer: Vec<MaybeUninit<T>> = Vec::with_capacity(size);
// Set length without initializing (unsafe!)
unsafe {
buffer.set_len(size);
}
// Initialize each element
for (i, elem) in buffer.iter_mut().enumerate() {
elem.write(init_fn(i));
}
// Convert to Vec<T>
unsafe {
let ptr = buffer.as_mut_ptr() as *mut T;
let len = buffer.len();
let cap = buffer.capacity();
std::mem::forget(buffer);
Vec::from_raw_parts(ptr, len, cap)
}
}
fn main() {
let numbers = initialize_buffer(10, |i| (i * 2) as i32);
println!("Numbers: {:?}", numbers);
}Building a Vec-style Structure
use std::mem::MaybeUninit;
struct MyVec<T> {
data: Vec<MaybeUninit<T>>,
len: usize,
}
impl<T> MyVec<T> {
fn with_capacity(capacity: usize) -> Self {
let mut data = Vec::with_capacity(capacity);
unsafe {
data.set_len(capacity);
}
Self { data, len: 0 }
}
fn push(&mut self, value: T) {
if self.len < self.data.len() {
self.data[self.len].write(value);
self.len += 1;
} else {
// Would need to reallocate
panic!("Capacity exceeded");
}
}
fn as_slice(&self) -> &[T] {
unsafe {
MaybeUninit::slice_assume_init_ref(&self.data[..self.len])
}
}
}
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
// Only drop initialized elements
for elem in &mut self.data[..self.len] {
unsafe {
elem.assume_init_drop();
}
}
}
}
fn main() {
let mut vec = MyVec::<String>::with_capacity(10);
vec.push(String::from("Hello"));
vec.push(String::from("World"));
println!("Slice: {:?}", vec.as_slice());
}Read and Write with Raw Pointers
use std::mem::MaybeUninit;
fn main() {
let mut uninit = MaybeUninit::<i32>::uninit();
// Get raw pointer for writing
let ptr = uninit.as_mut_ptr();
unsafe {
// Write through the pointer
ptr.write(123);
// Read back (safe because we just wrote)
let value = ptr.read();
println!("Value: {}", value);
}
// Or use the safe write method
let mut uninit2 = MaybeUninit::<String>::uninit();
uninit2.write(String::from("Hello"));
let s = unsafe { uninit2.assume_init() };
println!("String: {}", s);
}Copy from Slice
use std::mem::MaybeUninit;
fn main() {
let source = [1, 2, 3, 4, 5];
let mut dest: [MaybeUninit<i32>; 5] = MaybeUninit::uninit_array();
// Copy from a slice
MaybeUninit::copy_from_slice(&mut dest, &source);
// dest is now fully initialized
let initialized: [i32; 5] = unsafe {
MaybeUninit::array_assume_init(dest)
};
println!("Copied: {:?}", initialized);
}Conditional Initialization
use std::mem::MaybeUninit;
fn main() {
let condition = true;
let mut value = MaybeUninit::<String>::uninit();
let mut initialized = false;
if condition {
value.write(String::from("Initialized!"));
initialized = true;
}
// Only assume_init if we actually initialized
if initialized {
let s = unsafe { value.assume_init() };
println!("Got: {}", s);
} else {
println!("Not initialized");
// Don't call assume_init!
}
}Common Pitfalls
use std::mem::MaybeUninit;
fn main() {
// PITFALL 1: Reading before initialization
let uninit = MaybeUninit::<i32>::uninit();
// unsafe { uninit.assume_init() } // UB! Not initialized!
// PITFALL 2: Partial initialization of structs with Drop
struct HasDrop { x: i32 }
impl Drop for HasDrop {
fn drop(&mut self) { println!("Dropped"); }
}
// If you only partially initialize, drop won't work correctly
// PITFALL 3: Using zeroed for types with niches
// let bad: MaybeUninit<&i32> = MaybeUninit::zeroed();
// unsafe { bad.assume_init() } // UB! Null reference!
// CORRECT USAGE:
let mut good = MaybeUninit::<i32>::uninit();
good.write(42);
let value = unsafe { good.assume_init() };
println!("Correctly initialized: {}", value);
}Dropping Uninitialized Memory
use std::mem::MaybeUninit;
fn main() {
let mut uninit = MaybeUninit::<String>::uninit();
// Write a value
uninit.write(String::from("Hello"));
// Drop the value without taking it
unsafe {
uninit.assume_init_drop();
}
// Now uninit is back to uninitialized state
// Can safely be dropped without double-free
println!("Value dropped properly");
}Transmute with MaybeUninit
use std::mem::MaybeUninit;
fn main() {
// MaybeUninit has the same size and layout as T
// It's safe to transmute between them (but usually unnecessary)
let value: i32 = 42;
// These are equivalent:
let uninit1: MaybeUninit<i32> = MaybeUninit::new(value);
// transmute (advanced use only)
// let uninit2: MaybeUninit<i32> = unsafe {
// std::mem::transmute(value)
// };
println!("Size of MaybeUninit<i32>: {}", std::mem::size_of::<MaybeUninit<i32>>());
println!("Size of i32: {}", std::mem::size_of::<i32>());
// Same size!
}Summary
MaybeUninit Methods:
| Method | Description | Safety |
|---|---|---|
uninit() |
Create uninitialized memory | Safe |
zeroed() |
Create zero-initialized memory | Safe (but zero may be invalid) |
new(value) |
Create from initialized value | Safe |
write(value) |
Initialize with value | Safe |
assume_init() |
Extract the value | Unsafe |
assume_init_drop() |
Drop without extracting | Unsafe |
as_ptr() |
Get *const T |
Safe |
as_mut_ptr() |
Get *mut T |
Safe |
uninit_array() |
Create uninitialized array | Safe (const fn) |
array_assume_init() |
Convert array to initialized | Unsafe |
slice_assume_init_ref() |
Convert slice to reference | Unsafe |
slice_assume_init_mut() |
Convert mut slice to mut ref | Unsafe |
Safety Checklist:
| Action | Safe? |
|---|---|
Creating MaybeUninit::uninit() |
ā Safe |
Writing with .write(value) |
ā Safe |
Reading with .assume_init() |
ā ļø Unsafe, requires initialization |
Dropping with .assume_init_drop() |
ā ļø Unsafe, requires initialization |
Using zeroed() for primitive types |
ā Safe |
Using zeroed() for reference types |
ā UB (null reference) |
When to Use MaybeUninit:
| Scenario | Appropriate? |
|---|---|
| FFI with out parameters | ā Yes |
| Large array initialization | ā Yes |
| Custom collections | ā Yes |
| Performance-critical buffers | ā Yes |
| Normal variable initialization | ā Use normal binding |
| Simple struct creation | ā Use normal initialization |
Comparison with Related Types:
| Type | Initialization | Drop | Use Case |
|---|---|---|---|
T |
Required | Automatic | Normal values |
MaybeUninit<T> |
Manual | Manual | Uninitialized memory |
ManuallyDrop<T> |
Required | Manual | Prevent auto-drop |
UnsafeCell<T> |
Required | Automatic | Interior mutability |
Key Points:
MaybeUninit<T>is for safely handling uninitialized memory- Never read from
MaybeUninituntil fully initialized assume_init()is unsafe and requires initialization to be complete- Use
zeroed()only for types where all-zeros is valid - Essential for FFI, large arrays, and custom collections
- Same size as
Tbut no drop until you explicitly call it - Combines well with
ManuallyDropfor complex memory management
