DEV Community

이관호(Gwanho LEE)
이관호(Gwanho LEE)

Posted on

📝 Implements `Box<T>` and `Vec<T>` and why?

📦 What Is Box<T> (A Smart Help Pointer)?

Box<T> is a smart pointer that owns and stores its contents on the heap. It’s a core part of Rust’s ownership and memory management model, giving you fine control over data placement without sacrificing safety.

When you create a value with Box::new(x), Rust:

  1. Allocates memory on the heap
  2. Writes x into that memory
  3. Returns a Box<T> that points to the heap

✅ When to Use Box<T>

  • You need to store large data on the heap
  • You want to build recursive types (like trees or linked lists)
  • You’re working with trait objects and need dynamic dispatch (Box<dyn Trait>)
  • You want to transfer ownership safely across contexts

🛠️ A Simple Reimplementation of Box<T>

Want to know how Box<T> works internally? Here's a simplified, educational version:

use std::alloc::{alloc, dealloc, Layout};
use std::ptr::NonNull;

pub struct MyBox<T> {
    ptr: NonNull<T>,
}

impl<T> MyBox<T> {
    pub fn new(value: T) -> Self {
        let layout = Layout::new::<T>();
        let ptr = unsafe { alloc(layout) as *mut T };

        if ptr.is_null() {
            panic!("Allocation failed");
        }

        unsafe {
            ptr.write(value);
        }

        MyBox {
            ptr: unsafe { NonNull::new_unchecked(ptr) },
        }
    }

    pub fn get(&self) -> &T {
        unsafe { self.ptr.as_ref() }
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        let layout = Layout::new::<T>();
        unsafe {
            std::ptr::drop_in_place(self.ptr.as_ptr());
            dealloc(self.ptr.as_ptr() as *mut u8, layout);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🧠 How It Works (Step-by-Step)

  • Layout::new::<T>() tells the allocator how much space to reserve
  • alloc() allocates raw heap memory
  • ptr.write(value) writes the value into that memory
  • NonNull<T> wraps the pointer for safety (non-null guarantee)
  • Drop is implemented to automatically clean up the memory when the box is dropped

🧩 Example Use

fn main() {
    let boxed = MyBox::new(99);
    println!("Heap value: {}", boxed.get());
}
Enter fullscreen mode Exit fullscreen mode

🔍 Why This Matters for System Programming

Understanding how Box<T> is built gives you insights into:

  • Manual memory management (without garbage collection)
  • Why Rust avoids double frees and leaks
  • How to build your own allocators or smart pointers

And it’s especially critical for fields like:

  • Blockchain development
  • Operating system work
  • Embedded systems
  • Performance-critical network services

✅ Final Thoughts

Box<T> looks simple — but behind it is an elegant, powerful system for safely managing heap memory. Learning how it works isn't just an academic exercise: it’s one of the first steps to mastering real Rust systems programming.

“When you understand the memory, you control the performance.”

Why Vec<T> A Growable Heap Array ?

understanding how Vec<T> works under the hood is crucial. It's more than just a dynamic array: it's an efficient, memory-safe, growable buffer backed by heap memory. In this post, we’ll walk through what Vec<T> is, how it works, when to use it, and even how to reimplement a minimal version (MyVec<T>) to deepen your mastery.


📦 What Is Vec<T>?

Vec<T> is a growable, contiguous vector type that stores elements on the heap. It keeps track of:

  • A pointer to the buffer
  • The current length (len) — how many elements are initialized
  • The capacity — how many elements the buffer can hold before needing to grow

This is ideal for situations where:

  • You don’t know the size of your data at compile time
  • You want fast, indexed access
  • You may frequently add or remove elements

🧠 What Happens When You Call push()?

When you call v.push(x), here’s what actually happens:

  1. If the length equals capacity, Rust grows the buffer (usually doubling the capacity)
  2. It calculates the address at ptr.add(len) — the next free slot
  3. It writes the value using unsafe pointer write (ptr.write(value))
  4. It increments len
pub fn push(&mut self, value: T) {
    if self.len == self.capacity() {
        self.grow();
    }
    unsafe {
        let end = self.as_mut_ptr().add(self.len);
        end.write(value);
    }
    self.len += 1;
}
Enter fullscreen mode Exit fullscreen mode

Likewise, pop() just moves the length back by 1 and reads the last element from memory:

pub fn pop(&mut self) -> Option<T> {
    if self.len == 0 {
        return None;
    }
    self.len -= 1;
    unsafe {
        let ptr = self.as_mut_ptr().add(self.len);
        Some(ptr.read())
    }
}
Enter fullscreen mode Exit fullscreen mode

🔍 Memory Layout of Vec<T>

Buffer: [10][20][30][_][_][_]
Index :  0   1   2         capacity = 6
                      ↑
                   len = 3
Enter fullscreen mode Exit fullscreen mode
  • All values are stored contiguously
  • Insertions and deletions at the end are amortized O(1)

🛠️ Let’s Reimplement a Simplified MyVec<T>

Here’s a basic version of a vector that grows dynamically:

use std::alloc::{alloc, dealloc, realloc, Layout};
use std::ptr::NonNull;
use std::mem;

pub struct MyVec<T> {
    ptr: NonNull<T>,
    len: usize,
    cap: usize,
}

impl<T> MyVec<T> {
    pub fn new() -> Self {
        MyVec {
            ptr: NonNull::dangling(),
            len: 0,
            cap: 0,
        }
    }

    pub fn push(&mut self, value: T) {
        if self.len == self.cap {
            self.grow();
        }
        unsafe {
            let end = self.ptr.as_ptr().add(self.len);
            end.write(value);
        }
        self.len += 1;
    }

    fn grow(&mut self) {
        let (new_cap, new_layout) = if self.cap == 0 {
            (4, Layout::array::<T>(4).unwrap())
        } else {
            let new_cap = self.cap * 2;
            (new_cap, Layout::array::<T>(new_cap).unwrap())
        };

        let new_ptr = if self.cap == 0 {
            unsafe { alloc(new_layout) as *mut T }
        } else {
            let old_layout = Layout::array::<T>(self.cap).unwrap();
            unsafe { realloc(self.ptr.as_ptr() as *mut u8, old_layout, new_layout.size()) as *mut T }
        };

        self.ptr = NonNull::new(new_ptr).expect("allocation failed");
        self.cap = new_cap;
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            return None;
        }
        self.len -= 1;
        unsafe { Some(self.ptr.as_ptr().add(self.len).read()) }
    }

    pub fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len {
            return None;
        }
        unsafe { Some(&*self.ptr.as_ptr().add(index)) }
    }
}

impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        unsafe {
            for i in 0..self.len {
                std::ptr::drop_in_place(self.ptr.as_ptr().add(i));
            }
            let layout = Layout::array::<T>(self.cap).unwrap();
            dealloc(self.ptr.as_ptr() as *mut u8, layout);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

✨ Why Study the Internals of Vec<T>?

Understanding how Vec<T> works teaches you:

  • How safe systems manage dynamic memory manually
  • Why Rust avoids buffer overflows, dangling pointers, and double frees
  • How real containers grow, shrink, and manage performance-critical memory

This is especially useful for Rust developers who want to:

  • Build high-performance tools
  • Write custom memory allocators or data structures
  • Work in blockchain, embedded, OS, or graphics domains

🧠 Final Thoughts

By understanding Vec<T>, you’re not just using Rust — you’re learning how it thinks. You're seeing the boundaries between safe code and unsafe code — and how Rust lets you cross those lines carefully, with purpose.

“The more you understand how memory moves, the more power you have as a programmer.”

Stay curious. Try building your own vector. And the next time you call .push(), know exactly how it works behind the scenes.

Top comments (0)