heck yes — let’s button up your vector with real-world panic safety, capacity APIs, and then wrap the book with testing/benching and what’s next. same rustonomicon vibe, same unsafe disclaimers.
🧱 Reinventing from Scratch: Building Vec<T> the Hard Way
Chapter 8 — Panic Safety & Invariants
“When things fall apart, great containers don’t.”
8.1 the invariants (re-stated)
your MyVec<T> must always respect:
len <= cap- slots
[0..len)are initialized; slots[len..cap)are uninit - each initialized element is dropped exactly once
panic safety means: these must hold even if a panic occurs mid-operation.
8.2 capacity management: reserve / reserve_exact
growing on every push is bad. users should pre-reserve space.
impl<T> MyVec<T> {
pub fn reserve(&mut self, additional: usize) {
let needed = self.len.checked_add(additional).expect("overflow");
if needed > self.buf.capacity() {
self.grow_to(Self::grow_amortized(self.buf.capacity(), needed));
}
}
pub fn reserve_exact(&mut self, additional: usize) {
let needed = self.len.checked_add(additional).expect("overflow");
if needed > self.buf.capacity() {
self.grow_to(needed);
}
}
fn grow_amortized(current: usize, needed: usize) -> usize {
// typical strategy: double, but never less than needed
let double = if current == 0 { 4 } else { current * 2 };
double.max(needed)
}
fn grow_to(&mut self, new_cap: usize) {
// move reallocation into RawVec for clarity
self.buf.grow_to(new_cap);
}
}
and wire this in RawVec:
impl<T> RawVec<T> {
pub fn grow_to(&mut self, new_cap: usize) {
use std::alloc::{alloc, realloc, Layout};
unsafe {
let new_layout = Layout::array::<T>(new_cap).unwrap();
let new_ptr = if self.cap == 0 {
alloc(new_layout)
} else {
let old_layout = Layout::array::<T>(self.cap).unwrap();
realloc(self.ptr.cast(), old_layout, new_layout.size())
} as *mut T;
if new_ptr.is_null() { panic!("allocation failed"); }
self.ptr = new_ptr;
self.cap = new_cap;
}
}
}
⚠️ sidenote: OOM behavior the standard library’s
Vectypically aborts on OOM (via the global oom handler). our handmade version panics on null for pedagogy. for production, consider a fallible API (try_reserve) returningResult<(), AllocError>.
8.3 panic guards: making complex ops exception-safe
operations like insert perform two effects: shift elements, then write the new one. if constructor/move panics mid-shift, you must leave the vector in a consistent state.
pattern: scope a guard that restores invariants if we unwind.
struct LenGuard<'a, T> {
v: &'a mut MyVec<T>,
// how many new elements have been logically added (initialized)
inc: usize,
}
impl<'a, T> LenGuard<'a, T> {
fn new(v: &'a mut MyVec<T>) -> Self { Self { v, inc: 0 } }
fn bump(&mut self, n: usize) { self.inc += n; }
}
impl<'a, T> Drop for LenGuard<'a, T> {
fn drop(&mut self) {
// if unwinding, roll back uncommitted inits
unsafe {
while self.inc > 0 {
self.inc -= 1;
self.v.len -= 1;
std::ptr::drop_in_place(self.v.buf.ptr().add(self.v.len));
}
}
}
}
use it in insert:
impl<T> MyVec<T> {
pub fn insert(&mut self, index: usize, value: T) {
assert!(index <= self.len);
if self.len == self.buf.capacity() { self.buf.grow(); }
unsafe {
let ptr = self.buf.ptr();
// create guard *before* mutating len
let mut guard = LenGuard::new(self);
// shift right: copy [index..len) -> [index+1..len+1)
std::ptr::copy(ptr.add(index), ptr.add(index + 1), self.len - index);
// write new value; this **initializes** one more slot
std::ptr::write(ptr.add(index), value);
self.len += 1;
guard.bump(1); // we initialized exactly one element
// success: forget guard so it doesn't roll back
std::mem::forget(guard);
}
}
}
now, if anything panics after the write but before function exit, LenGuard’s Drop will revert the partially-added element and keep your invariants true.
💡 tip: similar guards can wrap multi-step algorithms (e.g.,
drain,splice, batchextend) to drop newly-initialized items if a later step panics.
8.4 shrink_to_fit, clear, and friends
tighten memory or fast-reset the vector without touching capacity logic.
impl<T> MyVec<T> {
pub fn clear(&mut self) {
unsafe {
// drop initialized part only
for i in 0..self.len {
std::ptr::drop_in_place(self.buf.ptr().add(i));
}
}
self.len = 0;
}
pub fn shrink_to_fit(&mut self) {
// optional: match cap to len; beware of churn if called too often
if self.len == self.buf.capacity() { return; }
self.buf.grow_to(self.len.max(1)); // keep at least 1 to avoid oscillation
}
}
⚠️ warning:
shrink_to_fitis a non-binding hint in std; doing it aggressively can harm performance due to re-alloc churn.
8.5 summary
- reserve APIs decouple logical ops from capacity growth.
- panic guards keep
lenand initialization boundaries truthful during unwinding. - never drop outside
[0..len); never leave initialized items beyondlen.
panic safety mantra: if it might panic, snapshot, do, guard, then commit.