Smart Pointers & Memory#

Smart pointers are types that act like pointers but have additional metadata and capabilities. They manage memory automatically through ownership and the Drop trait. Understanding them is essential for complex data structures, interior mutability, and interop with C.


Box — Heap Allocation#

Box<T> puts a value on the heap. The Box itself (a pointer) lives on the stack.

let b = Box::new(5);
println!("{b}");  // auto-derefs to i32

When to Use Box#

Use CaseWhy
Recursive typesCompiler needs a known size
Large dataAvoid stack overflow or expensive copies
Trait objectsBox<dyn Trait> for dynamic dispatch
Transferring ownershipWhen you need a heap allocation

Recursive Types#

// ERROR: size of List is infinite
// enum List { Cons(i32, List), Nil }

// OK: Box gives a fixed-size pointer
enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

Rc — Reference Counting (Single-Threaded)#

Multiple ownership via reference counting. The value is dropped when the last Rc is dropped.

use std::rc::Rc;

let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a);  // increment reference count (cheap, no deep copy)
let c = Rc::clone(&a);

println!("count: {}", Rc::strong_count(&a));  // 3
drop(c);
println!("count: {}", Rc::strong_count(&a));  // 2

Rc<T> is NOT thread-safe. Use Arc<T> for multi-threaded code.

Weak References#

Rc::downgrade creates a weak reference that does not prevent dropping:

use std::rc::{Rc, Weak};

let strong = Rc::new(42);
let weak: Weak<i32> = Rc::downgrade(&strong);

// Upgrade weak to strong (may fail if value is dropped)
if let Some(value) = weak.upgrade() {
    println!("{value}");
}

drop(strong);
assert!(weak.upgrade().is_none());  // value is gone

Use weak references to break reference cycles (e.g., parent ↔ child in a tree).


Arc — Atomic Reference Counting (Thread-Safe)#

Same as Rc<T> but uses atomic operations for the reference count:

use std::sync::Arc;

let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);

std::thread::spawn(move || {
    println!("{:?}", data_clone);
});

Arc alone only gives shared immutable access. For mutation, combine with Mutex or RwLock.


Interior Mutability#

Normal Rust: you cannot mutate through an immutable reference. Interior mutability patterns move the borrow check to runtime.

Cell — Copy Types Only#

use std::cell::Cell;

let cell = Cell::new(42);
cell.set(100);             // mutate through &Cell
let value = cell.get();    // 100

No references to the inner value — only get/set. Only works for Copy types.

RefCell — Runtime Borrow Checking#

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);

{
    let mut v = data.borrow_mut();  // runtime mutable borrow
    v.push(4);
}  // borrow released

let v = data.borrow();  // runtime immutable borrow
println!("{:?}", *v);    // [1, 2, 3, 4]

// PANICS at runtime if rules are violated:
// let a = data.borrow();
// let b = data.borrow_mut();  // panic: already borrowed

When to Use Which#

TypeThread SafeBorrow CheckCopy RequiredUse
Cell<T>✗None (get/set)✓Simple counters, flags
RefCell<T>✗Runtime✗Complex single-threaded mutation
Mutex<T>✓Runtime (lock)✗Multi-threaded mutation
RwLock<T>✓Runtime (read/write lock)✗Multiple readers, single writer

Rc<RefCell> — Shared Mutable Single-Threaded#

use std::rc::Rc;
use std::cell::RefCell;

let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

let clone1 = Rc::clone(&shared);
clone1.borrow_mut().push(4);

let clone2 = Rc::clone(&shared);
println!("{:?}", clone2.borrow());  // [1, 2, 3, 4]

Cow — Clone on Write#

Cow (Clone on Write) holds either a borrowed reference or an owned value. It clones only when mutation is needed:

use std::borrow::Cow;

fn process(input: &str) -> Cow<str> {
    if input.contains("bad") {
        // Need to modify — clone into owned String
        Cow::Owned(input.replace("bad", "good"))
    } else {
        // No modification needed — just borrow
        Cow::Borrowed(input)
    }
}

let result = process("this is fine");     // Cow::Borrowed — no allocation
let result = process("this is bad");      // Cow::Owned — allocated

Use Cow when most inputs pass through unchanged but some need modification.


Unsafe Rust#

unsafe lets you bypass the borrow checker for operations the compiler cannot verify:

unsafe {
    // These are the ONLY things unsafe allows:
    // 1. Dereference raw pointers
    // 2. Call unsafe functions
    // 3. Access mutable statics
    // 4. Implement unsafe traits
    // 5. Access union fields
}

Raw Pointers#

let x = 42;
let raw = &x as *const i32;       // immutable raw pointer
let raw_mut = &mut x as *mut i32;  // mutable raw pointer

// Creating raw pointers is safe. Dereferencing them is unsafe.
unsafe {
    println!("{}", *raw);
}

When Unsafe Is Appropriate#

AppropriateNot Appropriate
FFI (calling C code)Working around the borrow checker
Hardware accessAvoiding Clone/Arc costs
Performance-critical intrinsicsConvenience
Implementing data structures (linked lists)Anything safe Rust can express

The Unsafe Contract#

unsafe does not mean “no rules.” It means “I (the programmer) am responsible for maintaining the invariants the compiler normally checks.” The surrounding safe API must ensure that no safe code can cause undefined behaviour through the unsafe code.


Memory Layout#

use std::mem;

mem::size_of::<i32>()        // 4 bytes
mem::size_of::<&str>()       // 16 bytes (ptr + len)
mem::size_of::<String>()     // 24 bytes (ptr + len + cap)
mem::size_of::<Vec<i32>>()   // 24 bytes (ptr + len + cap)
mem::size_of::<Option<&str>>()  // 16 bytes (niche optimization — no extra tag)
mem::size_of::<Box<i32>>()   // 8 bytes (just a pointer)

Niche Optimisation#

Option<&T> is the same size as &T because the compiler uses the null value (which a reference can never be) to represent None. This also works for Box<T>, NonZeroU32, etc.


Key Takeaways#

  • Box<T> allocates on the heap. Use it for recursive types, large data, and trait objects.
  • Rc<T> enables shared ownership (single-threaded). Arc<T> is the thread-safe version.
  • RefCell<T> moves borrow checking to runtime — panics on violation instead of compile error.
  • Cow<T> avoids unnecessary cloning — borrows when possible, owns when necessary.
  • unsafe is a tool, not a escape hatch. Use it for FFI, hardware, and data structures — with a safe wrapper.
  • Rust’s smart pointers compose: Arc<Mutex<T>> for thread-safe shared mutation, Rc<RefCell<T>> for single-threaded.