So with the recent expansion of Cell
to move-based operations, we decided to leave potential further expansions to operations that require running “user code” to future work.
My thought process
I’d been idly musing since then that the right way to think about this is that &Cell
is effectively a “third kind of reference” alongside &
and &mut
. Cell
itself is opaque: every operation on Cell<T>
is in terms of &Cell
(except for those which convert between it and plain T
), you can’t really do anything separately with the &
and Cell
parts of it, they act as a single unit. The core requirement for memory safety is that you can’t simultaneously have a mutable reference to a memory structure together with a reference to something inside of it (because mutation of the structure itself can invalidate the interior reference), and &
, &mut
, and &Cell
each impose a different set of restrictions to ensure this:
-
&
says you can have as many outer and inner references as you like, but you can’t mutate. -
&mut
says you can mutate, and you can take inner references, but only one reference can be active at a time. -
&Cell
says you can have any number of outer references and you can mutate, but you can’t take inner references at all.*
So essentially, multiple references, inner references, mutation: choose any two, with the above being the 3 possible answers.
Basically Cell
is a fundamental language primitive masquerading as a standard library type.
All of this is a longwinded way of getting around to the point that I think "extensions of Cell
" to provide further operations for specific types logically belong in the modules for those types, rather than in std::cell
.
For instance, I’d been thinking that you could provide Clone
for Rc
s and Arc
s inside Cell
s by having methods such as Rc::clone_cell(&Cell<Rc<T>>) -> Rc<T>
. Or if we wanted to make that more general, we could have some kind of trait CellClone { fn clone_cell(&Cell<T>) -> T }
, which is a variant of Clone
where the self
pointer type is &Cell
.
All of that makes sense but it’s not really interesting or worth posting about.
The actual idea
So today I was randomly thinking about this and wondering if there wasn’t a cleaner way to do it, and I noticed that you could just write “T: CloneCell
” as Cell<T>: Clone
instead, which is functionally equivalent, and we can provide these impl
s:
impl<T> Clone for Cell<Rc<T>>
impl<T> Clone for Cell<Arc<T>>
This is equivalent in terms of expressivity to clone_cell
from above, because Clone
returns an owned Cell
, and if you want to access the Rc
or Arc
directly you can just call get_mut
or into_inner
.
I think this is quite nice because it’s extremely straightforward, doesn’t involve any actual new API surface so there’s nothing to bikeshed over, and my impression is that being able to do clone()
s of Rc
s and Arc
s inside Cell
s is like 80% of the Cell
pain point, and it solves that. I think it’s also forwards-compatible should we want to generalize it to more types later.
This doesn’t solve the “whole problem” because you still can’t clone, e.g., a Cell<(Rc<T>, Rc<T>)>
, not to mention other operations besides Clone
, but we’re no closer to solving the general problem (or agreeing on a solution) than we were before, and there’s little cost and significant gain to providing these impl
s in the meantime, so I think we should just go ahead and do that.