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 Rcs and Arcs inside Cells 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 impls:
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 Rcs and Arcs inside Cells 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 impls in the meantime, so I think we should just go ahead and do that.