Owning Raw Pointers

Excellent 😌 — we’ll go all the way down the rabbit hole. Below is a complete Rustonomicon-style book plan for the entire “Reinventing from Scratch — Building Vec<T> the Hard Way” series. I’ll expand Chapter 3 in full after this overview, then continue chapter-by-chapter with the same narrative style, side notes, and warnings.


🧩 Reinventing from Scratch

How to Build Vec<T> the Hard Way

Chapter Title Core Idea
1 What Even Is a Vector? Concept, layout, anatomy
2 Manual Memory Management 101 alloc, Layout, and raw heap
3 Owning Raw Pointers why Vec uses *mut T, ownership and aliasing
4 The RawVec Abstraction splitting allocator logic from element logic
5 Constructing Vec<T> implementing push, pop, insert, remove
6 Drop, Ownership & Safety correct destruction, ManuallyDrop, leaks
7 Iterators & Slices borrowing, slicing, IntoIter
8 Panic Safety & Invariants how to stay safe during partial failure
9 Testing & Benchmarking comparing to std Vec
10 Lessons Learned reflection and transition to Box, Rc, HashMap

⚙️ Chapter 3 — Owning Raw Pointers

“You don’t truly own data until you can free it, drop it, and move it — without breaking reality.”


🧭 3.1 Why Not Use Box<T>?

At first glance, Box<[T]> already gives you a heap array. So why does Vec<T> re-invent it?

Because Box is a static allocation — its size is fixed once created. Vec needs to grow and shrink. That means:

  • Re-allocating memory,
  • Moving ownership of raw pointers,
  • Writing into uninitialized regions.

And Box doesn’t let you do that safely — it always assumes full initialization.

So Vec drops the training wheels and goes straight to *mut T.


⚠️ Sidenote — The Pointer Zoo

Kind Meaning Ownership
&T immutable borrow temporary view
&mut T mutable borrow temporary exclusive access
Box<T> owned heap pointer safe, fixed size
Rc<T> reference counted shared ownership
*mut T / *const T raw pointers you are responsible

Once you use *mut T, the compiler stops helping you track lifetimes or aliasing. You promise by hand:

“No one else will mutate or drop this memory while I’m touching it.”


🧩 3.2 Creating a Smart Raw Pointer Wrapper

Let’s separate pointer + capacity handling into its own helper:

use std::alloc::{alloc, dealloc, realloc, Layout};

pub struct RawVec<T> {
    pub ptr: *mut T,
    pub cap: usize,
}

Initialization

impl<T> RawVec<T> {
    pub fn new() -> Self {
        RawVec {
            ptr: std::ptr::null_mut(),
            cap: 0,
        }
    }

    pub fn with_capacity(cap: usize) -> Self {
        let layout = Layout::array::<T>(cap).unwrap();
        let ptr = unsafe { alloc(layout) } as *mut T;
        if ptr.is_null() {
            panic!("allocation failed");
        }
        RawVec { ptr, cap }
    }
}

Growing

impl<T> RawVec<T> {
    pub fn grow(&mut self) {
        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(new_layout)
            } else {
                let old_layout = Layout::array::<T>(self.cap).unwrap();
                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;
        }
    }
}

Dropping

impl<T> Drop for RawVec<T> {
    fn drop(&mut self) {
        if self.cap != 0 {
            let layout = Layout::array::<T>(self.cap).unwrap();
            unsafe { dealloc(self.ptr.cast(), layout) };
        }
    }
}

Now RawVec<T> knows how to own and free memory, but not what lives inside it.


💡 3.3 Pointer Arithmetic — The Good, The Bad, and The UB

Rust guarantees:

ptr.add(i)

moves the pointer as if stepping through an array — but only if the entire [0, i] range lies within or one-past the allocated block.

Violating that = Undefined Behavior.

⚠️ Unsafe Rule #2: Never create a pointer to memory you don’t own, and never cross your block’s borders — not even to wave hello.


🧠 3.4 Constructing Values in Place

std::ptr::write(ptr, val) → writes val into uninitialized memory without dropping the previous contents.

std::ptr::read(ptr) → reads val out without dropping it (you now own that value).

These are the low-level primitives all containers rely on.


🧩 3.5 Ownership Invariants You Must Preserve

Every safe Vec maintains three sacred truths:

  1. len <= cap — never index beyond capacity.
  2. Elements [0…len) are initialized, rest are garbage.
  3. All initialized elements are dropped exactly once.

Break any one → instant UB.

You are now the high priest of these invariants.


🧹 3.6 Integrating RawVec into MyVec

pub struct MyVec<T> {
    buf: RawVec<T>,
    len: usize,
}

Now allocation and freeing happen automatically via RawVec’s Drop, and MyVec only handles logical length and element writes.

That separation mirrors the real std Vec design.


🧭 3.7 Visualization

MyVec<T>
 ├── buf: RawVec<T>
 │     ├── ptr → heap block
 │     └── cap = 8
 └── len = 5

The pointer is the physical memory. len is the logical truth about initialized elements.


✅ 3.8 Checkpoint

At this stage, you can:

  • Allocate / grow / free heap memory (RawVec)
  • Track how many slots are live (len)
  • Write and read via pointer arithmetic

You are one step away from a working, if dangerous, Vec.


Next Chapter → 4. The RawVec Abstraction We’ll polish this helper into a reusable allocator core, add proper error handling, and integrate it seamlessly with our MyVec.


If you’d like, I’ll continue immediately with Chapter 4, keeping the same “Rustonomicon tone” — sidebars, warnings, visual diagrams, and annotated code — and then proceed through all remaining chapters in sequence until Chapter 10. Would you like me to start writing Chapter 4: The RawVec Abstraction next?