Less restrictive mutability rules, again

i am surprised that nobody has mentionned it, but unsafe_cell_access will stabilize in 1.100 ( 12 November, 2026):

impl<T> UnsafeCell<T> {
    pub unsafe fn replace(&self, new_value: T) -> T;
}
impl<T: ?Sized> UnsafeCell<T> {
    pub unsafe fn as_ref_unchecked(&self) -> &T;
    pub unsafe fn as_mut_unchecked(&self) -> &mut T;
}

i have not read the whole, very long conversation, but all i can say without is i completely agree that using raw pointers when references are available is really annoying and leads to more bugs, which no doubt is why this feature exists.

that being said as_ref_unchecked and as_mut_unchecked have their own pitfalls, as they can easily lead to unsound overlaps. but they are as good as they can be.

it is of course usually better to use safe APIs such as Cell, though the lack of projections can be frustrating

2 Likes

Wow. I started this discussion, and it went off in an unexpected direction, of things that work like Cell but are less restrictive. Most of those involve some compromise to safety.

What I was looking for were ways to make safe code for references more ergonomic. Then maybe go on to making it faster. There's enough power with Rc, Weak, RefCell, downgrade, upgrade, borrow, and borrow_mut to do most useful reference operations. But writing Rust in that style leads to verbose code and the risk of failed upgrade or borrow operations.

So my thinking was a kind of single-owner Rc, called, perhaps, OwnerRc. The main difference is simply that OwnerRc is not copyable or cloneable. So it's for single ownership.

Prohibiting cloning means you can then enforce the rule that no other reference, Weak or strong, can still exist when an OwnerRc is dropped. That's a dangling pointer check at drop time. With that check in place, upgrade of a Weak reference can never fail. So upgrade can just return &T, rather than Option<&T>. That improves performance a bit and eliminates the need to write foo.upgrade().expect("Uh oh, foo was dropped.") That's a minor win, and I got that far.

The next step: If foo is an OwnerRc<RefCell<sometype>> you still have to borrow after upgrading The borrow can fail due to a conflict with another borrow. This is an obstacle to writing Rust in an Rc-heavy style. So, questions:

  1. Can we improve the ergonomics in a way that reduces the need to write .borrow() all over the place? Implicitly borrow on field access?
  2. Any hope of moving that borrow check to compile time? Conceptually, if all borrows of a type have such narrow scope that they never overlap, no borrow can fail. But if you write code like that by hand, there are a huge number of .borrow() and .borrow_mut() calls.

That's what I wanted to address. Thanks.

The lack of cloning doesn't put some restriction on drop order. The only way this would fail to compile...

let rc: OwnerRc<_> = ...;
let weak: Weak<_> = OwnerRc::downgrade(&rc);
drop(rc);
let still_alive = Weak::upgrade(&weak);

...is if weak borrowed rc, which I'm sure you don't want (you'd just use &*rc).

UniqueRc exists but doesn't function how you describe.

UniqueRc implements DerefMut which gets you a &mut RefCell<T> which gets you a &mut T.

I was unclear. The restriction on other pointers outliving the OwnerRc can be done with a run-time check of the reference counts and a panic in drop. It's not checked at compile time, but at least it's doesn't have to be checked at every .upgrade().

UniqueRc is someone else thinking along roughly the same lines, trying to improve weak circular references. I need to study the tracking issue on that.

It'd have to be an abort to be sound, as unwinding panics can be caught and dismissed.

May need to make this type non UnwindSafe.. I'd accept this kind of error, a dangling reference, as an abort condition.

Panics in destructors usually end badly. Exceptions in destructors never worked well in C++, and they tried to make that work. Rust doesn't have exceptions mostly because exception soundness is so hard.

Despite the name unwind safety is just a lint and cannot be relied upon for ensuring memory safety.

2 Likes

Actually, a "Data Race" is undefined behavior. A "Race Condition" is not undefined behavior. These are two different things.

Another old topic but I feel an unwrap/unwrap_deref operator solves most of pain?

We add a (pair of) new trait UnwrapDeref[Mut], callable with .!! (please don't bikeshed what exact it should look like).

So we just sugar weak.upgrade().unwrap().foo to weak.!!foo, cell.borrow_mut().bar() to cell.!!bar(). Or chained together, using the same mechainism of autoderef: weak_cell.!!bar() would desugar to weak.upgrade().unwrap().borrow_mut().bar(), assumming mut needed. If UnwrapDeref[Mut] and Deref[Mut] both exists, we choose UnwrapDeref[Mut].

If we need to store UnwarpDeref[Mut]'s return value to a variable, maybe just invent another unary operator like $! (again please don't bikeshed this, I know $! is bad.).

Edit: Yes I know some people hate unwrap and don't want unwrap being easily written.