rust-lang / rust-lang/libs-team

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

Open
#660 19 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

ACP-accepted api-change-proposal
Dominant language
Rust
Stars
178
Forks
28
Avg merge
15m
Merged PRs (30d)
1

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 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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the existing std::rc::Rc and Weak APIs, especially Rc::new_cyclic, then review PR #140640 and the linked libs-team and internals discussions. Done means the deferred-initialization API and its safety, lifecycle, and behavior requirements are agreed and reflected in the implementation and tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.