Growth Strategies and Reallocation

Excellent — let’s move deeper into the machine. We now stand between two worlds: the heap allocator and the container logic. To build a truly reusable Vec, we’ll isolate memory management into its own mini-engine — just like std::Vec does internally.


🧩 Reinventing from Scratch: Building Vec<T> the Hard Way

Chapter 4 — The RawVec Abstraction

“Safety isn’t free; it’s layered. And the first layer is a box that knows how to grow without breaking.”


🧭 4.1 Why Split Vec and RawVec ?

A real-world Vec actually has two halves:

Layer Job
RawVec Owns and manages the raw heap memory (capacity, allocation, growth)
Vec Owns logical length, element initialization, and drop logic

Why? Because allocation is orthogonal to initialization. When you grow or shrink, you don’t want to touch every element — you just need a new box.

⚠️ If allocation logic and element logic mingle, you’ll eventually double-free or leak.


🧩 4.2 Our RawVec Blueprint

We already built a rough version in Chapter 3. Now we’ll refactor it to mimic how std::RawVec (the internal one) behaves.

use std::alloc::{self, Layout};

pub struct RawVec<T> {
    ptr: *mut T,
    cap: usize,
}

impl<T> RawVec<T> {
    pub fn new() -> Self {
        Self { ptr: std::ptr::null_mut(), 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 { alloc::alloc(layout) } as *mut T;

        if ptr.is_null() {
            panic!("allocation failed");
        }

        Self { ptr, cap }
    }

    pub fn capacity(&self) -> usize {
        self.cap
    }

    pub fn ptr(&self) -> *mut T {
        self.ptr
    }
}

We’ve defined a contract:

RawVec<T> promises valid ownership of a heap region large enough for cap × T, but doesn’t care what lives inside it.”


💡 Sidenote — The Allocator’s Responsibilities

A healthy RawVec must uphold three invariants:

  1. cap == 0ptr is null.
  2. Non-null ptr ⇒ points to a valid block sized for cap.
  3. Deallocation uses the exact same Layout that allocation used.

Break #3 and you enter UB country.


⚙️ 4.3 Growing and Shrinking — The Engine Room

impl<T> RawVec<T> {
    pub fn grow(&mut self) {
        // Choose new capacity
        let new_cap = if self.cap == 0 { 4 } else { self.cap * 2 };
        let new_layout = Layout::array::<T>(new_cap).unwrap();

        unsafe {
            let new_ptr = if self.cap == 0 {
                alloc::alloc(new_layout)
            } else {
                let old_layout = Layout::array::<T>(self.cap).unwrap();
                alloc::realloc(self.ptr.cast(), old_layout, new_layout.size())
            } as *mut T;

            if new_ptr.is_null() {
                panic!("realloc failed");
            }

            self.ptr = new_ptr;
            self.cap = new_cap;
        }
    }

    pub fn shrink_to_fit(&mut self, used: usize) {
        // optional: reduce wasted memory
        if used == 0 {
            unsafe {
                if self.cap != 0 {
                    let layout = Layout::array::<T>(self.cap).unwrap();
                    alloc::dealloc(self.ptr.cast(), layout);
                }
            }
            *self = Self::new();
        } else {
            let layout = Layout::array::<T>(used).unwrap();
            unsafe {
                let new_ptr = alloc::realloc(self.ptr.cast(),
                    Layout::array::<T>(self.cap).unwrap(),
                    layout.size()) as *mut T;
                self.ptr = new_ptr;
                self.cap = used;
            }
        }
    }
}

⚠️ Unsafe Corner

realloc is deceptively simple but lethal:

  • If it fails → your old memory is still valid (but you’ve lost track if you overwrite the pointer).
  • If you panic mid-grow and drop old elements twice → 💥 UB.

That’s why the real Vec has complex try_reserve and panic-guard logic. For us mortals, we accept a little unsafety to learn how the gods work.


🧹 4.4 Cleaning Up

impl<T> Drop for RawVec<T> {
    fn drop(&mut self) {
        if self.cap != 0 {
            unsafe {
                let layout = Layout::array::<T>(self.cap).unwrap();
                alloc::dealloc(self.ptr.cast(), layout);
            }
        }
    }
}

RawVec now behaves like a mini-allocator. It knows how to create, resize, and destroy a heap region. It does not know what’s inside — and that’s exactly the point.


📦 4.5 Composing It with MyVec

Now MyVec can delegate all heavy lifting:

pub struct MyVec<T> {
    buf: RawVec<T>,
    len: usize,
}

impl<T> MyVec<T> {
    pub fn new() -> Self {
        Self { buf: RawVec::new(), len: 0 }
    }

    pub fn with_capacity(cap: usize) -> Self {
        Self { buf: RawVec::with_capacity(cap), len: 0 }
    }

    pub fn capacity(&self) -> usize { self.buf.capacity() }
}

We’ve separated capacity management (in RawVec) from element management (in MyVec). That’s modularity and safety.


💡 Visualization

MyVec<T>
 ├── buf: RawVec<T>
 │     ├── ptr → heap block
 │     └── cap = 8
 └── len = 3

When MyVec drops, its RawVec drops automatically, freeing the heap.


⚙️ 4.6 Edge Cases to Fear

Scenario What Can Go Wrong
cap = 0 but ptr != null Deallocating a ghost block
cap > 0 but ptr = null Dangling pointer
Wrong layout on dealloc Heap corruption
Overlapping realloc Data loss or double free

🔥 Unsafe Rule #3: Your memory bookkeeping is your life. Miscount once, and you’re in undefined territory forever.


🧠 4.7 Summary

Concept Purpose
RawVec<T> Abstracts heap allocation
grow() Reallocates when capacity is full
shrink_to_fit() Optional optimization
Drop Ensures cleanup
MyVec = RawVec + length The complete dynamic array foundation

Next → Chapter 5: Constructing Vec<T> We’ll finally implement push, pop, insert, remove and watch our vector come alive with real elements and real drops.


Would you like me to continue directly with Chapter 5: Constructing Vec<T> in the same detailed, illustrated Rustonomicon tone?