Insert, Remove, and Drain

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:

  1. len <= cap
  2. slots [0..len) are initialized; slots [len..cap) are uninit
  3. 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 Vec typically aborts on OOM (via the global oom handler). our handmade version panics on null for pedagogy. for production, consider a fallible API (try_reserve) returning Result<(), 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, batch extend) 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_fit is 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 len and initialization boundaries truthful during unwinding.
  • never drop outside [0..len); never leave initialized items beyond len.

panic safety mantra: if it might panic, snapshot, do, guard, then commit.