Smart Pointers

Shared Ownership

Shared ownership is an ownership topology where a single heap-allocated value is owned simultaneously by two or more handles, and the value remains alive until the final handle drops. In Rust, shared ownership is represented explicitly through reference-counted smart pointers: Rc<T> for single-threaded graphs and Arc<T> for multi-threaded sharing.

Tree Ownership vs. Directed Acyclic Graph (DAG) Ownership

With Box<T> or direct struct fields, every value has a unique parent, forming a strict tree hierarchy. When domain models require a Directed Acyclic Graph (DAG) — such as multiple build targets depending on the same shared library node, or multiple AST expressions referencing the same interned symbol — unique ownership forces either data duplication or lifetime-bound arena references. Reference-counted pointers allow multiple parent nodes to co-own the same child node without tying the graph to a lexical lifetime 'a.

Unique Ownership (Tree via Box<T>)       Shared Ownership (DAG via Rc<T> / Arc<T>)
        [Root]                                 [App A]           [App B]
        /    \                                    \                 /
       v      v                                    v               v
  [Child 1] [Child 2]                             [Shared Core Library]
                                                   (strong_count = 2)
use std::rc::Rc;

#[derive(Debug)]
struct Package {
    name: String,
    dependencies: Vec<Rc<Package>>,
}

fn main() {
    let core_lib = Rc::new(Package {
        name: String::from("serde"),
        dependencies: vec![],
    });

    let service_a = Package {
        name: String::from("auth-service"),
        dependencies: vec![Rc::clone(&core_lib)],
    };

    let service_b = Package {
        name: String::from("billing-service"),
        dependencies: vec![Rc::clone(&core_lib)],
    };

    println!("core_lib owners: {}", Rc::strong_count(&core_lib));
    println!(
        "{} -> {}",
        service_a.name, service_a.dependencies[0].name
    );
    println!(
        "{} -> {}",
        service_b.name, service_b.dependencies[0].name
    );
}
core_lib owners: 3
auth-service -> serde
billing-service -> serde

Why Shared Ownership Defaults to Immutable Access

Rust's aliasing rule requires that an object never have an active mutable reference (&mut T) at the same time as any other reference (&T or &mut T). Because Rc<T> and Arc<T> allow arbitrary aliases to coexist at runtime, unconditionally providing &mut T through two Rc<T> clones would violate aliasing and cause undefined behavior.

Consequently:

  • Rc<T> and Arc<T> provide shared &T access by default.
  • Mutating shared data requires pairing the shared pointer with an interior mutability primitive that enforces borrowing or locking rules at runtime:
    • Single-threaded: Rc<Cell<T>> or Rc<RefCell<T>>
    • Multi-threaded: Arc<Mutex<T>>, Arc<RwLock<T>>, or Arc<Atomic*>

Choosing Between Borrowing (&'a T) and Shared Ownership (Rc<T> / Arc<T>)

Criterion Borrowed Reference (&'a T) Shared Ownership (Rc<T> / Arc<T>)
Owner lifetime Single lexical owner must outlive all &'a T No single owner; allocation lives until last handle drops
Struct type signature Requires lifetime parameter struct View<'a> Self-contained ('static-compatible, no lifetime contagion)
Runtime cost Zero (plain pointer, no counter updates) Heap allocation + counter increment/decrement on clone/drop
Async / thread spawning Cannot satisfy 'static bound required by thread::spawn or tokio::spawn Satisfies 'static bound (Arc<T> for multi-threaded tasks)
Cyclic graphs Cannot form self-referentialowned cycles easily Can form cycles; requires Weak<T> back-edges to avoid leaks

Zero-Copy Slice Sharing with Rc<[T]> and Arc<[T]>

When sharing immutable buffers (such as network packets, audio samples, or configuration blobs), converting a Vec<T> into Rc<[T]> or Arc<[T]> via .into() creates a reference-counted slice without double indirection (the elements sit directly inside the reference-counted heap block alongside the strong/weak counters).

use std::sync::Arc;

fn main() {
    let buffer: Arc<[u8]> = Arc::from([10_u8, 20, 30, 40]);
    let worker_handle = Arc::clone(&buffer);

    println!("size of Arc<[u8]> fat pointer: {} bytes", size_of_val(&buffer));
    println!("shared buffer contents:        {:?}", worker_handle);
}
size of Arc<[u8]> fat pointer: 16 bytes
shared buffer contents:        [10, 20, 30, 40]