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 forcap×T, but doesn’t care what lives inside it.”
💡 Sidenote — The Allocator’s Responsibilities
A healthy RawVec must uphold three invariants:
cap == 0⇒ptris null.- Non-null
ptr⇒ points to a valid block sized forcap. - Deallocation uses the exact same
Layoutthat 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);
}
}
}
}
✅
RawVecnow 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 implementpush,pop,insert,removeand 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?