Smart Pointers

Smart pointers manage ownership and sharing of heap-allocated data with automatic cleanup.

Overview

TypeThread-safeUse CaseEnergy Cost
Box<T>N/AHeap allocation, recursive typesAllocation only
Rc<T>NoSingle-threaded shared ownership3.0 pJ clone/drop
Arc<T>YesMulti-threaded shared ownership3.0 pJ clone/drop (atomic)
Cow<T>N/AClone-on-write optimizationFree reads, allocation on write

Box<T>

Heap-allocated value. Required for recursive types. Box<T> is a pointer in memory — zero overhead beyond the allocation.

// Recursive type requires Box
pub enum Expr {
    Literal(i32),
    Add { left: Box<Expr>, right: Box<Expr> },
    Neg { inner: Box<Expr> },
}

let expr = Expr::Add {
    left: Box::new(Expr::Literal(1)),
    right: Box::new(Expr::Literal(2)),
};

Methods

let b = Box::new(42);
let inner = b.into_inner();    // 42 — consumes the Box
let r: &i32 = b.as_ref();     // borrow the inner value
let r: &mut i32 = b.as_mut(); // mutable borrow
let ptr = b.leak();            // leak memory, return raw pointer

Rc<T>

Reference-counted pointer for single-threaded shared ownership. Multiple Rc<T> values can point to the same data. The data is freed when the last Rc is dropped.

let a = Rc::new(42);
let b = a.clone();            // increment reference count (3.0 pJ)
let c = a.clone();            // count is now 3

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

// When a, b, c all go out of scope, the value is freed

Methods

let rc = Rc::new(vec![1, 2, 3]);
let count = Rc::strong_count(&rc);     // number of references
let inner = Rc::into_inner(rc);        // unwrap if count == 1
let r: &Vec<i32> = rc.as_ref();       // borrow inner value

// Mutable access (only if count == 1)
let mut rc = Rc::new(42);
if let Option::Some(val) = Rc::get_mut(&mut rc) {
    *val = 100;
}

Use Case: Shared Graph Nodes

pub struct Node {
    pub value: i32,
    pub children: Vec<Rc<Node>>,
}

let leaf = Rc::new(Node { value: 1, children: Vec::new() });
let parent = Node {
    value: 0,
    children: vec![leaf.clone(), leaf.clone()],  // shared ownership
};

Arc<T>

Atomically reference-counted pointer for multi-threaded shared ownership. Same API as Rc<T>, but uses atomic operations for thread safety.

use std::concurrency::spawn;

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

let handle = spawn(|| {
    let local = data.clone();      // atomic increment (3.0 pJ)
    println!("len = {}", local.len());
});

println!("len = {}", data.len());  // still valid in main thread

Methods

let arc = Arc::new(42);
let count = Arc::strong_count(&arc);   // number of references
let r: &i32 = arc.as_ref();           // borrow inner value
let cloned = arc.clone();             // atomic increment

// Arc::get_mut — only if count == 1
// Arc::into_inner — unwrap if count == 1
// Arc::make_mut — clone inner if shared, then return &mut

Energy: Rc vs Arc

OperationRcArc
clone3.0 pJ (increment)3.0 pJ (atomic increment)
drop3.0 pJ (decrement + conditional free)3.0 pJ (atomic decrement + conditional free)
as_ref0 pJ (pointer deref)0 pJ (pointer deref)

Use Rc when data stays on one thread. Use Arc when sharing across threads. The energy cost is similar, but Arc incurs cache-line contention overhead under high concurrency.

Cow<T>

Clone-on-write smart pointer. Wraps either a borrowed reference or an owned value. Reading is free; writing clones the data only if it's currently borrowed.

// Start with a borrowed value
let text = Cow::borrowed("hello");
println!("{}", text.as_ref());        // free — no allocation

// Convert to owned only when needed
let owned = text.to_owned();          // allocates if borrowed

// Check state
text.is_borrowed();   // true
text.is_owned();      // false

Methods

let cow = Cow::borrowed("hello");
let cow2 = Cow::owned("world".to_string());

let r: &str = cow.as_ref();           // borrow — always free
let s: String = cow.into_owned();     // consume, clone if borrowed
let owned = cow.to_owned();           // clone if borrowed, return owned Cow

cow.is_borrowed();                     // true if wrapping a reference
cow.is_owned();                        // true if wrapping an owned value

Use Case: Conditional Transformation

fn normalize(input: &str) -> Cow<str> {
    if input.contains(' ') {
        // Only allocate when we actually need to modify
        Cow::owned(input.replace(' ', "_"))
    } else {
        // No allocation — return a reference to the original
        Cow::borrowed(input)
    }
}

// Most inputs pass through without allocation
let a = normalize("hello");     // Cow::borrowed — 0 allocation
let b = normalize("hello world"); // Cow::owned — 1 allocation

Choosing a Smart Pointer

  • Need heap allocation for recursive types? Use Box<T>
  • Need shared ownership on one thread? Use Rc<T>
  • Need shared ownership across threads? Use Arc<T>
  • Need to avoid cloning until mutation? Use Cow<T>
  • Need unique ownership? Just use the value directly (no pointer needed)