-
Notifications
You must be signed in to change notification settings - Fork 24
Description
Proposal
Problem statement
Today, building reference-counted cyclic graphs in std with Rc is possible but constrained: Rc::new_cyclic requires completing the cycle inside a single closure, which prevents spanning await points and makes staged or multi‑phase initialization awkward. This pushes developers toward mutation‑heavy patterns (e.g., UniqueRc with post‑construction setters, Option/MaybeUninit fields, or RefCell) that are error‑prone, can accidentally form strong cycles, and impose runtime costs on every access. There is no standard, zero‑overhead facility to allocate an Rc once, thread Weak links where needed, and initialize exactly once later, while preserving Rc/Weak invariants without exposing unsafe internals.
Motivating examples or use cases
- Asynchronous cyclic construction: allocate nodes, pass
Weakhandles acrossawaitpoints to connect edges incrementally, then initialize once the graph is fully wired, whichRc::new_cycliccannot express because it must close the cycle in a single closure. - Long chain or DAG builds without deep recursion or mutation: construct long linked lists or multi‑stage graphs iteratively, avoiding stack growth and post‑allocation “setter” patterns that require
Option/MaybeUninit/RefCellworkarounds. - Safer construction that prevents accidental strong cycles: by separating allocation from one‑shot initialization, there is no need to temporarily install
Rcplaceholders or later mutate fields, reducing the risk of creatingRc<RefCell<_>>cycles during setup.
Solution sketch
Introduce std::rc::RcUninit<T>, a deferred‑initialization handle that reserves Rc storage first and initializes it exactly once later, returning an Rc<T>.
-
Type and placement
pub struct RcUninit<T>instd::rc.- Lives in
stdbecauseRc/Weaklayout and invariants are opaque; a sound, zero‑overhead deferred‑init mechanism must integrate with those invariants, which cannot be reproduced in a crate without relying on unstable internals.
-
Core API (sketch)
fn new() -> RcUninit<T>: allocateRcbacking without constructingT.fn weak(&self) -> Weak<T>: produce aWeak<T>during the uninitialized phase to thread edges into other nodes before initialization.fn init(self, value: T) -> Rc<T>: consumeRcUninitto initialize exactly once and returnRc<T>; double‑init is prevented by the type, and dropping an uninitializedRcUninitruns noTdestructor.
-
Semantics
- Weak before init:
Weak::upgradereturnsNonebeforeinit, andSome(Rc<T>)afterinitwhile the allocation is alive. Send/Sync: mirrorsRc(i.e.,!Send/!Sync).
- Weak before init:
Alternatives
-
Rc::new_cyclic- Works for simple cycles created entirely inside one closure, but cannot span
awaitpoints or multi‑phase wiring, and can require awkward nesting to thread all edges at once.
- Works for simple cycles created entirely inside one closure, but cannot span
-
UniqueRcplus setters- Forces mutation‑based setup and field placeholders (
Option,MaybeUninit,Weak) that add runtime overhead (unwrap/clone, upgrade) and complexity, and increases the risk of creating strong cycles inadvertently (e.g.,Rc<RefCell<_>>used during setup).
- Forces mutation‑based setup and field placeholders (
-
External crates
- Cannot achieve the same safety and zero‑overhead because
Rc/Weakinternals are not public; any external approach would either reimplement a parallel RC or rely on unsafe knowledge of std internals, which defeats the purpose of a standard facility. This is the key reason the feature should live instd. - Crate rcuninit is one implementation but makes brittle assumptions about data layout in std.
- Cannot achieve the same safety and zero‑overhead because
Links and related work
- PR: Implement
RcUninit(#112566) — Implement RcUninit (#112566) rust#140640 - RcUninit was discussed in
Cyclic Arc and Rc Builder #90
https://internals.rust-lang.org/t/an-alternative-to-rc-new-cyclic/22849/6
Tracking Issue forUniqueRc/UniqueArcrust#112566