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>>— returnsSomeif the target is still live;Noneif the lastSharedwas dropped.Weakdoesn't keep the allocation alive; cycles involving onlyShared+Weakfree correctly.
Typical cases
| Relationship | Pattern |
|---|---|
| Tree (parents own children) | parents Shared, children Weak back-pointer |
| Observer | subject keeps List<Weak<Observer>>; observers hold Shared<Subject> (or nothing) |
| Cache with weak refs to loaded values | Map<Key, Weak<Value>> |
| Graph with shared nodes | Shared<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>isSendandSyncwhenT: 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
- base → memory —
Heap,Shared,Weak. - mem — implementation details.
- Shared state — multi-task mutation patterns.