Excellent 👏
Now that our Vec works, it’s time to talk about its dark twin: destruction.
Creation is easy; safe destruction is what separates real engineers from chaos demons.
This chapter is about how Rust ensures everything gets dropped exactly once — and how to maintain that promise when you’re the one writing the unsafe code.
⚰️ Reinventing from Scratch: Building Vec<T> the Hard Way
Chapter 6 — Drop, Ownership & Safety
“Everything that is created must eventually be dropped. The question is: who, when, and how many times?”
🧭 6.1 Why Drop Is Dangerous
In normal Rust, ownership rules guarantee each value is dropped exactly once.
But when you use raw pointers and manual allocation, the compiler can’t see what’s initialized, what’s moved, or what’s leaked. You can:
- Drop a value twice → 💀 undefined behavior
- Forget to drop it → 🧠 memory leak
- Drop an uninitialized value → 🌀 undefined behavior again
So in MyVec, we have to manually enforce drop discipline.
🧩 6.2 How Vec<T> Drops Things
When your Vec goes out of scope, Rust automatically calls its Drop implementation.
That does two things:
- Iterates over
[0..len)→ callsdrop_in_place()on each initialized element. - Frees the heap memory using
dealloc()viaRawVec’s drop.
In our version:
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));
}
}
// Then RawVec::drop() runs automatically
}
}
This works fine most of the time.
But when T itself owns heap memory (like String, Box, Vec), things can get tricky.
💣 6.3 When Things Go Wrong
Scenario 1: Panic During Growth
If grow() panics (say alloc fails or resizing triggers an OOM handler), we might have:
- Half the elements moved to a new buffer,
- The old buffer not yet freed,
lentemporarily inconsistent.
If you drop this half-moved state → double free. If you forget to drop → leak.
⚠️ Panic Safety Rule: No destructor should ever see partially moved memory.
The real Vec handles this with RawVec + guard structs, but for learning purposes, we’ll rely on Drop discipline and careful ordering.
Scenario 2: Partially Filled Vector
Imagine you reserve space for 10 items but only initialize 3. You must drop only those 3, not the remaining 7 uninitialized slots.
That’s why len matters so much.
Vec never drops beyond len, and that’s the single most important invariant of all.
🧠 6.4 Meet ManuallyDrop<T>
Sometimes, you don’t want an object to be dropped automatically (e.g., because you’ll drop it manually later).
That’s what ManuallyDrop is for.
use std::mem::ManuallyDrop;
let s = ManuallyDrop::new(String::from("hello"));
drop(s); // doesn’t drop the string!
Now you’re responsible for calling ManuallyDrop::drop(s) yourself (via unsafe).
⚠️ Why This Matters
In Vec, internal buffers like RawVec use ManuallyDrop to ensure:
- The vector can be safely reallocated, even during panic.
- Dropping the wrapper doesn’t accidentally double-drop the data.
We can retrofit this idea into our MyVec:
use std::mem::ManuallyDrop;
pub struct MyVec<T> {
buf: RawVec<T>,
len: usize,
_marker: std::marker::PhantomData<ManuallyDrop<T>>,
}
Now we can use ManuallyDrop inside push/pop logic safely if needed.
🔬 6.5 The Lifecycle of a Single Element
Let’s trace one element (String) through the entire Vec lifecycle:
-
Push —
ptr::write(ptr.add(len), value)→ value moves into uninitialized heap slot →len += 1 -
Drop — Iterate over
[0..len)→drop_in_place(ptr.add(i))→ callsString’s destructor, freeing its heap memory -
Dealloc —
RawVecdrops, freeing the heap buffer.
Each layer cleans up its own domain — no overlap, no missing pieces.
📊 Visualization
push("Nepal") ─▶ heap: ["Nepal"]
push("India") ─▶ heap: ["Nepal", "India"]
drop() ─▶ drop_in_place both strings
dealloc() ─▶ free entire heap block
Every element dies once. No leaks, no ghosts.
⚙️ 6.6 Drop Hierarchy
Let’s see the full destructor chain:
Vec<T>::drop() → drops initialized elements
↓
RawVec<T>::drop() → deallocates memory
↓
Allocator backend → releases to the OS
This strict order ensures:
- Elements are gone before freeing memory.
- Freed memory never contains live values.
Dropcascades cleanly through ownership.
🧹 6.7 Avoiding Double Drop During pop() and remove()
Here’s a subtle mistake beginners make:
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 { return None; }
self.len -= 1;
Some(unsafe { *self.buf.ptr().add(self.len) }) // ❌ wrong
}
This reads the value and leaves it behind,
so when Drop runs later, Rust drops it again.
✅ Correct version:
Some(unsafe { std::ptr::read(self.buf.ptr().add(self.len)) })
ptr::read moves without dropping the source — preventing double-drops.
⚠️ 6.8 Debugging Drops (Mentally)
When writing unsafe containers, it’s often helpful to think in terms of ownership ledger entries.
| Action | Ownership Change |
|---|---|
push(value) |
moves value into heap → Vec now owns it |
pop() |
moves value out → caller owns it |
drop() |
drops all owned values, then memory |
Every value must be “owned exactly once” in that table — if you ever have two owners or zero, you’ve broken the invariant.
🧠 6.9 Chapter Summary
| Concept | Meaning |
|---|---|
Drop |
Cleans up resources deterministically |
drop_in_place |
Calls destructor manually |
ptr::read |
Moves without dropping |
ManuallyDrop |
Disables automatic destruction |
len |
Boundary between initialized & uninitialized |
Golden Rule: “Drop everything you initialized, but nothing you didn’t.”
🌱 6.10 Closing Thoughts
We now understand not just how to create values safely, but how to let them die safely. This is the soul of ownership in Rust — not about allocation, but about correctness at destruction.
🧩 Next → Chapter 7: Iterators & Slices
We’ll implement .iter(), .iter_mut(), .into_iter(), and as_slice() —
diving into Rust’s most powerful borrowing trick: slicing contiguous memory safely.
Would you like me to continue with Chapter 7: Iterators & Slices next — with diagrams for lifetimes, &[T] vs &mut [T], and implementation of iterators?