-
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
Weak
handles acrossawait
points to connect edges incrementally, then initialize once the graph is fully wired, whichRc::new_cyclic
cannot 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
/RefCell
workarounds. - Safer construction that prevents accidental strong cycles: by separating allocation from one‑shot initialization, there is no need to temporarily install
Rc
placeholders 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
std
becauseRc
/Weak
layout 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>
: allocateRc
backing 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>
: consumeRcUninit
to initialize exactly once and returnRc<T>
; double‑init is prevented by the type, and dropping an uninitializedRcUninit
runs noT
destructor.
-
Semantics
- Weak before init:
Weak::upgrade
returnsNone
beforeinit
, andSome(Rc<T>)
afterinit
while 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
await
points 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
-
UniqueRc
plus 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
/Weak
internals 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
/UniqueArc
rust#112566