Perfect 😌 Grab your unsafe gloves and safety goggles — because this is where our humble skeleton becomes alive.
We’ve built RawVec, the muscle of memory management.
Now it’s time to teach MyVec how to grow, push, pop, and drop — like a real, breathing Rust collection.
🧩 Reinventing from Scratch: Building Vec<T> the Hard Way
Chapter 5 — Constructing Vec<T>
“All the world’s safety rests upon one truth: initialized elements must live, uninitialized ones must sleep.”
🧭 5.1 The Final Structure
Let’s look at what our Vec is, at its core:
pub struct MyVec<T> {
buf: RawVec<T>, // manages heap allocation
len: usize, // number of initialized elements
}
We now have:
buf.ptr()→ the raw heap memory blockbuf.capacity()→ how much we can fitlen→ how much is actually used
That’s the same layout as Rust’s real Vec<T>.
⚠️ Sidenote — Invariants of a Safe Vector
Before we start coding, burn these three into your brain:
len <= capacity- Memory from
[0..len)is initialized - Memory from
[len..cap)is uninitialized
If we ever:
- Drop an uninitialized element, or
- Forget to drop an initialized one,
→ 💀 Undefined Behavior.
🧩 5.2 Implementing push()
Let’s add the ability to grow our vector by one element.
impl<T> MyVec<T> {
pub fn push(&mut self, value: T) {
// if we're full, double the capacity
if self.len == self.buf.capacity() {
self.buf.grow();
}
unsafe {
// write the element directly into the uninitialized slot
let end = self.buf.ptr().add(self.len);
std::ptr::write(end, value);
}
self.len += 1;
}
}
🧠 Key Idea:
std::ptr::write moves the value without dropping the old content (because there was none!).
This is how we safely initialize uninitialized memory.
⚙️ Example
let mut v = MyVec::with_capacity(2);
v.push(10);
v.push(20);
v.push(30); // triggers grow()
// heap: [10, 20, 30, ?, ?, ?]
🧹 5.3 Implementing pop()
Now the reverse — remove the last element.
impl<T> MyVec<T> {
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
self.len -= 1;
unsafe {
let end = self.buf.ptr().add(self.len);
Some(std::ptr::read(end))
}
}
}
🧠 Why ptr::read?
Because it copies the value out without dropping it at the source — you now own it.
If we used *end, Rust would think it’s still initialized and try to drop it later again → double drop 💣.
🧱 5.4 Implementing get() and get_mut()
Basic indexing, with bounds checking:
impl<T> MyVec<T> {
pub fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
unsafe { Some(&*self.buf.ptr().add(index)) }
} else {
None
}
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
if index < self.len {
unsafe { Some(&mut *self.buf.ptr().add(index)) }
} else {
None
}
}
}
⚠️ The dereference
&*ptr.add(i)is only safe because we ensuredindex < len, and all[0..len)are initialized. Break that — and the compiler can’t save you.
🪄 5.5 Implementing insert() and remove()
We’ll mimic Vec’s behavior: shifting elements to make space.
Insert
impl<T> MyVec<T> {
pub fn insert(&mut self, index: usize, value: T) {
assert!(index <= self.len, "index out of bounds");
if self.len == self.buf.capacity() {
self.buf.grow();
}
unsafe {
let ptr = self.buf.ptr();
// shift elements to the right
std::ptr::copy(
ptr.add(index),
ptr.add(index + 1),
self.len - index,
);
// place new value
std::ptr::write(ptr.add(index), value);
self.len += 1;
}
}
}
Remove
impl<T> MyVec<T> {
pub fn remove(&mut self, index: usize) -> T {
assert!(index < self.len, "index out of bounds");
unsafe {
let ptr = self.buf.ptr();
let value = std::ptr::read(ptr.add(index));
// shift elements left
std::ptr::copy(
ptr.add(index + 1),
ptr.add(index),
self.len - index - 1,
);
self.len -= 1;
value
}
}
}
⚠️ Pointer Dance Warning:
ptr::copyis memmove-like — overlapping regions are allowed. But if you mess uplenbookkeeping during a panic, you’ll drop garbage. RealVecuses panic guards to recover gracefully.
🧹 5.6 Implementing Drop
Finally, we must destroy initialized elements before freeing memory.
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
unsafe {
for i in 0..self.len {
std::ptr::drop_in_place(self.buf.ptr().add(i));
}
}
// `RawVec`’s Drop runs next and frees the heap.
}
}
That’s it — you now have a fully functioning Vec from scratch.
🧠 5.7 Summary
| Function | Role |
|---|---|
push |
Writes into next slot, grows if needed |
pop |
Reads & removes last element |
insert |
Shifts right, inserts new |
remove |
Reads element, shifts left |
Drop |
Drops all initialized elements |
get |
Safe read-only access |
🧩 Visualization
Before push:
[ _ _ _ _ ] len=0
After push(10,20,30):
[10 20 30 _] len=3
After remove(1):
[10 30 _ _] len=2
✅ Congratulations! You now have a functioning, dynamically resizing vector implemented entirely by hand. It allocates, grows, shifts, drops, and respects ownership invariants — all through raw pointers.
🧠 Next → Chapter 6: Drop, Ownership & Safety
We’ll explore how to handle destructors safely (String, Box, etc.), use ManuallyDrop, and prevent panic-induced double frees.
Would you like me to continue with Chapter 6: Drop, Ownership & Safety next — including all unsafe pitfalls, ManuallyDrop diagrams, and real-world examples (Vec<String>, etc.)?