Pop, Indexing, and Safe Access

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 block
  • buf.capacity() → how much we can fit
  • len → 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:

  1. len <= capacity
  2. Memory from [0..len) is initialized
  3. 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 ensured index < 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::copy is memmove-like — overlapping regions are allowed. But if you mess up len bookkeeping during a panic, you’ll drop garbage. Real Vec uses 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.)?