Skip to main content

Shared<T> + Weak<T>

Heap<T> is unique ownership; Shared<T> is multiple owners via atomic reference counting. Weak<T> breaks cycles.

When to reach for Shared

  • Multiple tasks need the same data.
  • You want the data freed automatically when no one references it.
  • You can't (or don't want to) impose a single owner.
let config: Shared<Config> = Shared.new(load_config());
let copy1 = config.clone(); // bumps strong count to 2
let copy2 = config.clone(); // 3
Shared.strong_count(&config); // -> 3

All clones point to the same heap allocation. The allocation is freed when the last Shared<Config> is dropped.

Shared.get_mut — safe interior mutation

let mut s: Shared<i32> = Shared.new(42);
*Shared.get_mut(&mut s).unwrap() += 1; // Some(&mut) when strong_count == 1

Returns Some(&mut T) only when no other Shared clone exists. For shared mutation, wrap in a lock: Shared<Mutex<T>>.

Cycles leak without Weak

Children pointing to parents via Shared creates a reference cycle that ARC can't break:

type Node is {
value: Int,
parent: Maybe<Shared<Node>>,
children: List<Shared<Node>>,
};
// Parent owns child; child owns parent. Neither refcount reaches 0.
// Memory leaks.

Break with Weak

type Node is {
value: Int,
parent: Maybe<Weak<Node>>, // ← non-owning
children: List<Shared<Node>>,
};

implement Node {
fn new(value: Int) -> Shared<Node> {
Shared.new(Node { value, parent: Maybe.None, children: List.new() })
}

fn add_child(parent: &Shared<Node>, child: Shared<Node>) {
// Give child a weak pointer back to parent.
let mut child_inner = Shared.get_mut(&mut child.clone()).unwrap();
child_inner.parent = Maybe.Some(Shared.downgrade(parent));
// Parent owns child.
parent.children.push(child);
}

fn parent(&self) -> Maybe<Shared<Node>> {
self.parent.as_ref().and_then(Weak.upgrade)
}
}
  • Shared.downgrade(&s) -> Weak<T> — creates a non-owning handle.
  • Weak<T>.upgrade() -> Maybe<Shared<T>> — returns Some if the target is still live; None if the last Shared was dropped.
  • Weak doesn't keep the allocation alive; cycles involving only Shared + Weak free correctly.

Typical cases

RelationshipPattern
Tree (parents own children)parents Shared, children Weak back-pointer
Observersubject keeps List<Weak<Observer>>; observers hold Shared<Subject> (or nothing)
Cache with weak refs to loaded valuesMap<Key, Weak<Value>>
Graph with shared nodesShared<Mutex<GraphData>> — central data, avoid cycles altogether

Count inspection

Shared.strong_count(&s) // current number of Shared clones
Shared.weak_count(&s) // current number of Weak clones

Thread-safety

  • Shared<T> is Send and Sync when T: Send + Sync.
  • Cloning / dropping is atomic.
  • Interior mutation still needs synchronisation — wrap in Mutex<T> / RwLock<T> / AtomicCell<T> for multi-task writes.

Pitfall — accidentally constructing a cycle

Always audit both directions: if a Shared<Parent> holds a List<Shared<Child>> and Shared<Child> holds a Shared<Parent>, you have a cycle. One of the two arrows must be Weak.

Why no separate single-threaded Rc<T>?

Verum exposes one ref-counted handle, Shared<T>, with atomic counters. The cost is one atomic-fetch-add on .clone() and one atomic-fetch-sub on drop — typically 1–2 ns on modern CPUs, and uncontended atomics on the same cache line are nearly free relative to the surrounding memory traffic.

For the rare case where the atomic cost matters in a tight single-threaded loop, prefer the inline alternatives — pass &T references, hold ownership behind a parent Heap<T>, or use Cow<T>::Borrowed. The cost model is then explicit at the type boundary rather than hidden behind a parallel ref-counted type.

See also