Skip to content

ACP: std::rc::RcUninit for deferred initialization and await-safe cyclic construction #660

@kstrafe

Description

@kstrafe

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 across await points to connect edges incrementally, then initialize once the graph is fully wired, which Rc::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 creating Rc<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> in std::rc.
    • Lives in std because Rc/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>: allocate Rc backing without constructing T.
    • fn weak(&self) -> Weak<T>: produce a Weak<T> during the uninitialized phase to thread edges into other nodes before initialization.
    • fn init(self, value: T) -> Rc<T>: consume RcUninit to initialize exactly once and return Rc<T>; double‑init is prevented by the type, and dropping an uninitialized RcUninit runs no T destructor.
  • Semantics

    • Weak before init: Weak::upgrade returns None before init, and Some(Rc<T>) after init while the allocation is alive.
    • Send/Sync: mirrors Rc (i.e., !Send/!Sync).

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.
  • 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).
  • 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 in std.
    • Crate rcuninit is one implementation but makes brittle assumptions about data layout in std.

Links and related work

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions