Skip to content

ACP: Add CovariantUnsafeCell #815

Description

@WaffleLapkin

Proposal

Problem statement

There are cases where UnsafeCell being invariant is too restrictive. Specifically, there are cases where a part of the data in an UnsafeCell is never written to, and thus doesn't have to be invariant.

Motivating examples or use cases

Lazy*

LazyLock and LazyCell are currently invariant in F. However, they could be covariant in F, since their APIs do not in any way allow writing a value of F — it is only ever read to be called during initialization.

However, the implementation cannot be changed in a way to make it covariant in F without sacrificing size:

pub struct LazyLock<T, F = fn() -> T> {
    once: Once,
    data: UnsafeCell<Data<T, F>>,
}

union Data<T, F> {
    value: ManuallyDrop<T>,
    f: ManuallyDrop<F>,
}

The way LazyLock works makes it so the only transition possible is from data containing f to containing value. Only value is ever written, so only T needs to be invariant. However, Data<T, F> needs to be in an UnsafeCell to make the initialization through &LazyLock sound, but that causes both T and F to be invariant.

One could implement a CovariantLazyLock by having two separate fields (rather than overlapping fields of a union), value: UnsafeCell<MaybeUninit<T>>, f: ManuallyDrop<F>, but that would waste memory.

Solution sketch

Basically the same as UnsafeCell, but covariant.

pub struct CovariantUnsafeCell<T: ?Sized> { ... }

impl<T> CovariantUnsafeCell<T> {
    pub const fn new(value: T) -> Self { ... }
    pub const fn into_inner(self) -> T { ... }
}

impl<T> CovariantUnsafeCell<T> {
    pub const fn get(&self) -> *mut T { ... }
    pub const fn get_mut(&mut self) -> &mut T { ... }
    pub const fn raw_get(this: *const Self) -> *mut T { ... }
}

Alternatives

  • We could unify CovariantUnsafeCell, SyncUnsafeCell, and UnsafeCell into something like UnsafeCell<T, S = Unsync, V = Invariant>
  • We could do nothing, as this is not a very common use-case (it does limit the expressiveness of the language though)

Links and related work

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    I-libs-api-nominatedIndicates that an issue has been nominated for discussion during a team meeting.T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions