On this page

Box, Rc, Arc

Box is single-owner heap allocation (simplest), Rc is single-threaded reference counting (shared ownership), Arc is multi-threaded atomic reference counting. Weak breaks reference cycles; in memory layout, the value and reference count of Rc/Arc are allocated adjacently on the heap—understanding these three is understanding Rust’s "no GC but with reference counting."

Box: Single-Owner Heap Allocation

let b = Box::new(42);               // Allocate an i32 on the heap
// stack: [8-byte ptr] → heap: [4-byte value: 42]

Box<T> is Rust's simplest heap allocation—single owner, no reference counting. When b goes out of scope, the heap memory is freed. Similar to C++'s std::unique_ptr<T>, but the Rust version doesn't require move semantics keywords—ownership is guaranteed at compile time.

When to Use Box

  1. Recursive Types: Rust must know the type size at compile time. enum List { Cons(i32, List), Nil } recursion leads to infinite size. Solution: Cons(i32, Box<List>)—Box has a fixed size (pointer, 8B), recursion is resolved indirectly via the heap.
  2. Trait Objects: Box<dyn Display> stores any type that implements Display.
  3. Moving Large Values from Stack to Heap: If you have [u8; 1000000], using Box::new([0u8; 1000000]) places only an 8-byte pointer on the stack.
  4. DST (Dynamically Sized Type): Box<[T]>, Box<str>, Box<dyn Trait>.

Rc: Single-Threaded Reference Counting

use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);     // strong_count = 1
let b = Rc::clone(&a);              // strong_count = 2, does not copy Vec data!
let c = Rc::clone(&a);              // strong_count = 3

Internal layout (allocated adjacently on the heap):

heap: [strong_count: usize(8B)] [weak_count: usize(8B)] [T: data]
Rc<T>: ptr → points to the address of T (actually points before strong_count)

Rc::clone only increments the strong_count—the data is not copied. When the last Rc<T> is dropped → strong_count = 0 → data is freed. Rc does not implement Send—its reference counting operations are not atomic, and using it across threads will cause data races.

Arc: Multi-Threaded Reference Counting

use std::sync::Arc;
let a = Arc::new(vec![1, 2, 3]);
let b = Arc::clone(&a);            // atomic increment → thread-safe
std::thread::spawn(move || {
    println!("{:?}", b);           // b can be moved into thread
});

Same internal structure as Rc, but strong/weak count operations use atomic instructions. Arc::clone is about 10x slower than Rc::clone (atomic increment vs non-atomic increment), but it is still extremely fast for heap allocation (~10ns vs ~1ns).

Weak: Preventing Reference Cycles

use std::rc::{Rc, Weak};
let strong = Rc::new(42);
let weak: Weak<_> = Rc::downgrade(&strong);  // Does not increase strong_count

drop(strong);                        // strong_count = 0 → data freed
assert!(weak.upgrade().is_none());   // Weak detects data has been freed → None

Classic use case: In tree structures, parent nodes store Weak references to child nodes. If they stored Rc, it would be a reference cycle—parent references child, child references parent, neither can be freed. Weak does not prevent data from being freed; it temporarily grants access to the data via the upgrade() method (which may return None).

References

  • Rust Book: Chapter 15.1-15.4
  • Rustonomicon: Arc atomicity, reference counting

Keywords: Box, Rc, Arc, Weak, reference counting, heap allocation, recursive type, DST, Drop, atomic