title: “Using NonNull in Box and Vec Implementations” meta_description: “Refactor Box and RawVec to use NonNull for clearer invariants and better safety in Rust.” keywords: [“rust box nonnull”, “rawvec nonnull”, “vec internals”, “smart pointers rust”]
🏗️ Reinventing from Scratch: NonNull<T>
Chapter 6 — “Building on Box and RawVec”
“Replace
*mut TwithNonNull<T>and watch your invariants become clearer.”
🧭 6.1. Before and After
Box with Raw Pointer
pub struct MyBox<T> {
ptr: *mut T, // Could be null!
}
impl<T> MyBox<T> {
pub fn new(value: T) -> Self {
// What if allocation returns null?
let ptr = unsafe { alloc(...) as *mut T };
// Need to check!
if ptr.is_null() {
panic!("Allocation failed");
}
// ...
}
}
Box with NonNull
pub struct MyBox<T> {
ptr: NonNull<T>, // Never null!
}
impl<T> MyBox<T> {
pub fn new(value: T) -> Self {
let ptr = unsafe {
let p = alloc(...) as *mut T;
NonNull::new(p).expect("Allocation failed");
// Or use new_unchecked if alloc never returns null
};
// Clearer: ptr is guaranteed non-null
// ...
}
}
Benefits:
- Invariant is type-level (not just documentation)
- No need to check null in every method
- Optimizer can make better assumptions
🎯 6.2. RawVec with NonNull
pub struct RawVec<T> {
ptr: NonNull<T>,
cap: usize,
}
impl<T> RawVec<T> {
pub fn new() -> Self {
Self {
ptr: NonNull::dangling(), // Zero-capacity!
cap: 0,
}
}
pub fn with_capacity(cap: usize) -> Self {
if cap == 0 {
return Self::new();
}
let layout = Layout::array::<T>(cap).unwrap();
let ptr = unsafe {
let p = alloc(layout) as *mut T;
NonNull::new(p).expect("Allocation failed")
};
Self { ptr, cap }
}
pub fn ptr(&self) -> *mut T {
self.ptr.as_ptr()
}
}
📦 6.3. Vec with NonNull
pub struct MyVec<T> {
buf: RawVec<T>, // Uses NonNull internally
len: usize,
}
impl<T> MyVec<T> {
pub fn new() -> Self {
Self {
buf: RawVec::new(), // ptr is dangling (non-null!)
len: 0,
}
}
pub fn push(&mut self, value: T) {
if self.len == self.buf.cap {
self.buf.grow();
}
unsafe {
// NonNull makes this clearer
let ptr = self.buf.ptr().add(self.len);
ptr.write(value);
}
self.len += 1;
}
pub fn get(&self, index: usize) -> Option<&T> {
if index >= self.len {
return None;
}
unsafe {
// Safe: index < len, ptr is valid
Some(&*self.buf.ptr().add(index))
}
}
}
✨ 6.4. Benefits of NonNull
Before: Defensive Null Checks
pub fn get(&self, index: usize) -> Option<&T> {
if self.ptr.is_null() { // Redundant if well-designed!
return None;
}
// ...
}
After: Clearer Intent
pub fn get(&self, index: usize) -> Option<&T> {
// No null check needed — NonNull guarantees it!
// Just check bounds
if index >= self.len {
return None;
}
// ...
}
🎨 6.5. Dangling vs Null
// Zero-capacity Vec
pub struct MyVec<T> {
ptr: NonNull<T>, // Dangling when cap == 0
len: usize,
cap: usize,
}
// When empty:
// ptr = NonNull::dangling() ← Non-null but invalid to deref
// len = 0
// cap = 0
// Key insight: We never deref when len == 0!
// So dangling pointer is safe
🧠 6.6. Chapter Takeaways
Refactoring to NonNull gives us:
- ✅ Type-level invariants (can’t be null)
- ✅ Clearer code (no defensive null checks)
- ✅ Better optimizations (compiler knows pointer is valid)
- ✅ Safer APIs (intent is explicit)
🚀 6.7. What’s Next
Next: Chapter 7 — Slices, Fat Pointers, and Struct Embedding
Learn how NonNull works with:
NonNull<[T]>(fat pointers with length)NonNull<dyn Trait>(fat pointers with vtable)- Embedded pointers in structs
💡 Exercises
- Refactor Box to use NonNull
- Refactor Vec/RawVec to use NonNull
- Compare codegen of *mut T vs NonNull
(use cargo-show-asm) - Measure performance difference (should be zero!)
- Build a linked list with Option<NonNull
>
Next: Chapter 7 — Slices, Fat Pointers, and Struct Embedding 📦