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
config.strong_count(); // -> 3

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

get_mut — safe interior mutation​

// The counts and the accessors are METHODS on the value, not
// associated functions taking a reference — there is no
// `Shared.get_mut(&mut s)`.
let mut s: Shared<Int> = Shared.new(42);
*s.get_mut().unwrap() += 1; // Some only when strong == 1 and weak == 0

Returns Some(&mut T) only when there is one strong reference and no weak references. is_unique() checks the strong count alone. Cloning a Shared handle keeps the same allocation and increments its strong count; it does not create a uniquely mutable copy of the value.

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​

This declaration sketch shows owning child edges and weak parent links:

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 parent(&self) -> Maybe<Shared<Node>> {
self.parent.as_ref().and_then(Weak.upgrade)
}
}

Updating a shared node needs an explicit mutation API. The ownership topology above does not make mutation through a cloned handle valid; clone().get_mut().unwrap() cannot establish unique access while the original strong handle remains live.

  • s.downgrade() -> 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​

s.strong_count() // current number of Shared clones
s.weak_count() // 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​