Drop, RAII, and Resource Cleanup

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:

  1. Iterates over [0..len) → calls drop_in_place() on each initialized element.
  2. Frees the heap memory using dealloc() via RawVec’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,
  • len temporarily 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:

  1. Pushptr::write(ptr.add(len), value) → value moves into uninitialized heap slot → len += 1

  2. Drop — Iterate over [0..len)drop_in_place(ptr.add(i)) → calls String’s destructor, freeing its heap memory

  3. DeallocRawVec drops, 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.
  • Drop cascades 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?