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.
Proposal
Problem statement
There are cases where
UnsafeCellbeing invariant is too restrictive. Specifically, there are cases where a part of the data in anUnsafeCellis never written to, and thus doesn't have to be invariant.Motivating examples or use cases
Lazy*LazyLockandLazyCellare currently invariant inF. However, they could be covariant inF, since their APIs do not in any way allow writing a value ofF— it is only ever read to be called during initialization.However, the implementation cannot be changed in a way to make it covariant in
Fwithout sacrificing size:The way
LazyLockworks makes it so the only transition possible is fromdatacontainingfto containingvalue. Onlyvalueis ever written, so onlyTneeds to be invariant. However,Data<T, F>needs to be in anUnsafeCellto make the initialization through&LazyLocksound, but that causes bothTandFto be invariant.One could implement a
CovariantLazyLockby 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.Alternatives
CovariantUnsafeCell,SyncUnsafeCell, andUnsafeCellinto something likeUnsafeCell<T, S = Unsync, V = Invariant>Links and related work
F[ofOnceCell], it's not a breaking change to make it co-variant later." StabilizeLazyCellandLazyLockrust#121377 (comment)Lazy<T, F>covariant inFmatklad/once_cell#167What 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):
Second, if there's a concrete solution: