Mutable vs exclusive reference

I did not get rauljordan's desire to call mutable references "exclusive references". mutable and exclusive are not the same thing, they are orthogonal concepts. One can have all combinations of mutable, or not, and exclusive, or not.

immutable not exclusive = OK allows multiple readers of data at same time.
immutable and exclusive = OK but not useful.
mutable not exclusive = BAD allows multiple writers of data at same time, race conditions etc.
mutable and exclusive = OK

As we see from the possibilities, if we want safe code then mutable implies exclusive. Exclusive does not imply mutable. Looks like "mutable reference" is the more suitable term.

Aside: How would we make an immutable exclusive reference if we wanted one for some odd reason?

Assuming you meant "mutable": yeah, but if you have an exclusive reference, you can safely mutate it, so not allowing that seems like a pointless restriction at that point.

You could wrap a &mut T in a wrapper type that acts as if it were an immutable reference (and is not Copy).

Generally, the recommendation to call them exclusive comes together with a recommendation to call immutable references for "shared". This naming makes sense since immutable references to things like Cell<usize> and AtomicUsize actually allow you to change the value.

More generally, when you want to modify something, you have to ask the question of how you avoid data races. There are several possible answers:

  1. My reference is exclusive. (&mut T)
  2. My reference is single-threaded. (&Cell<T>)
  3. My reference uses atomic instructions. (&AtomicUsize)

In each case, it is safe and allowed to mutate the value. However, the latter two references can be shared.

20 Likes

There's an interesting case of Mutex::get_mut (lets you access object without locking the mutex) that is safe because it takes an exclusive reference. It has to require &mut, even for cases where only read-only access is required. The same function taking & would be unsound.

And OTOH you can mutate mutex via &Mutex, so mutex can flip 180° the concepts of mutable vs immutable. However, the shared vs exclusive perspective remains the same.

There are odd cases for everything, e.g. &&mut is immutable even if you dereference it to &mut, fn<'a>(&'a mut self) -> &'a gives you an immutable reference, but because it shares a lifetime with an exclusive loan, there are additional restrictions on it.

But shared vs exclusive generally leads to fewer paradoxes. A shared cache would have fn set(&self, value), which is weird to mutate an object via "immutable" reference, but it makes sense if you just think about it as updating a shared cache.

Rust uses "interior mutability", which on a technical level is brilliant, but I don't like it as a term, because to me it sounds like calling blue color a "red with interior blueness".

5 Likes

Here's a post by Niko on the topic.

It mentions that closures can capture unique references to &mut T variables which were not themselves declared mut ("&uniq &mut T"). That's an exclusive immutable reference on the outside -- and you can mutate through it, unlike & &mut T, because it's exclusive. [1]

You can't have an exclusive &T because references are Copy.


I prefer the exclusive/shared nomenclature as otherwise every mention of "immutable borrows" is either incomplete or carries an asterisk to explain or hand-wave interior mutability. And I find the emphasis on exclusiveness makes it easier to explain &mut related borrow errors. Interior mutability I then prefer to call shared mutability. And as per that name (and @alice's concrete examples), mutability doesn't imply exclusive.


  1. I'm not sure edition 2021+ closures have these anymore -- as I understand it they reborrow references instead of taking references to references. ↩︎

6 Likes

Yes, Rust 2021 no longer use these. I couldn't find the post, but it was discussed here and there was some way to convince compiler to print an error message which revealed the difference.

1 Like

The unique borrows definitely still exist. E. g. Rust Playground

Uncomment the &x; line in main and see an error message talking about such borrows. Looks like it even explains why the whole variable was captured.

error[E0501]: cannot borrow `x` as immutable because previous closure requires unique access
  --> src/main.rs:13:5
   |
4  |     let mut c = || {
   |                 -- closure construction occurs here
5  |         let r: &&mut i32 = &x;
   |                             - first borrow occurs due to use of `x` in closure
6  |         println!("{:p}, {:p}", r, *r);
7  |         *x += 1;
   |         -- capture is immutable because of use here
...
13 |     &x;
   |     ^^ second borrow occurs here
14 |     c();
   |     - first borrow later used here

there's probably a typo in this error message though :joy:

it should probably say "capture is unique because of use here"?

3 Likes

This is wrong on two aspects:

  • you can have safe code with mutable shared references, they just need some kind of restriction to make them safe. See &Cell<T>, &RefCell<T>, &Mutex<T> ecc ecc. This also shows that you don't need a &mut reference to have mutation.

  • you assume you don't need an &mut reference to have an "immutable and exclusive" reference, but that's wrong, it still needs a &mut reference wrapped somewhere. So you can't have exclusivity without &mut references.

So in the end:

  • &mut is needed for "mutable and exclusive" and "immutable and exclusive"
  • & is needed for "immutable and not exclusive" and "mutable and not exclusive"

So the natural conclusion is that &mut is indeed an exclusive reference and & is a non-exclusive (i.e. shared) reference.

It's not clear to me how I can be wrong. Apart from the fact that you feel differently about it. My saying "looks like...more suitable" it should be clear I'm expressing my opinion. My opinions are not wrong, but they may differ from yours. I also offered support of my opinion via a logical argument that is sound, you are of course free to not value that train of thought. And "mutable references" is the terminology used by Rust rather than using words like "unique", "shared", "exclusive" or whatever, so I presume the Rust creators saw it the same way.

Indeed we can do that. And we don't need an &mut to do it. For example the typical use of mutex:

    let counter = Arc::new(Mutex::new(0));
    ...
    let mut num = counter.lock().unwrap();
    *num += 1;

No sign of any &mut mutable reference our counting variable there. Or is there? Yes there is, except it is called Mutex. Which is short for mutual exclusion. The upshot is that a mutual reference is there but to achieve safety the simultaneous mutability is prevented at run time through the Mutex rather than being enforced at compile time with &mut. Or at least that is the way I see it, correct me if I am wrong. At least the docs refer to "enforcing borrowing rules at runtime".

These discussions always confuse me. For example having first programmed in 1974 I did not hear of "interior mutability" until discovering Rust. Is that a term that actually means something in use outside of Rust? I presume things like the Mutex above are examples of interior mutability

The problem is that rustc still uses unique captures in 2018 mode, but normal mutable ones in 2021 mode (via reborrow), but most error messages are shared.

Thus I would trust them too much.

1 Like

A mutex is indeed an example of interior mutability.

In my opinion, needing to use weird phrases like "interior mutability" is a consequence of the decision to call references "mutable" and "immutable". If we called them "exclusive" and "shared", there would never be any need to have a special word for "immutable but actually it's mutable". It would just be a shared reference that had some mechanism that made mutation safe.

12 Likes

I noticed myself in subsequent testing, too, that the error messages are inaccurate in many other cases where actually re-borrows are used, but in the concrete example I linked, as far as I can tell, it is also clearly demonstrated that the closure actually contains a unique capture. As far as I can tell.

Thanks. Good to hear I'm not as confused as I think I am :slight_smile:

However I'm not sure I'm with you on the "exclusive", "shared" thing.

Now that you have fixed the "interior mutability" term in my mind it seems natural to think of Arc::new(Mutex::new(...)); as creating an immutable Arc/Mutex object that contains a mutable reference to something. Which is how I thought of it anyway, rather than thinking "immutable but actually it's mutable".

What I mean is that there are two things in play here the Mutex and the thing that is protected by the Mutex. One is immutable the other is not. It's not one thing that is "immutable but actually it's mutable".

If you see what I mean.

1 Like

I always find it hard to imagine just the whole picture of the "what if it was called shared and exclusive reference" scenario.
To name just one aspect as an example, Rust currently has mut annotations on local variables which function to allow (or prevent if absent) creation of &mut references. Arguably this feature is quite useful in the common case to help reason about code. If the terminology is different, in my view, it would be harder to convey to language learners why mut annotations correspond to allowing exclusive references. Or the annotations would need to be changed, too.

I mean, I'm assuming here, that the premise is that &mut references would have different syntax, otherwise, the name “mutable reference” would always be a good choice already based entirely on that syntax, wouldn't it? So it's something like &exc for exclusive or &uni for unique? What if we do use the same for the variable annotations? let exc n = 1;. But what's an “exclusive variable” and why do I have to use weird terminology in order to express whether or not my integer variable is mutable?

I could also imagine that a mutability system could be separate from the exclusivity system. So that then perhaps we do have all 4 types of references; mutable/immutable unique/shared references. But that would require additional design and I don't know whether or not we can expect a practically useful, not overly complicated system to come out of this. Maybe we can, I'm just saying that this contributes to my original premise of finding it hard to imagine what this whole scenario could look like.


Who knows, maybe I'm sufficiently deep into a Rust mindset that I don't see how perhaps the current situation with its "its immutable, except in certain special cases where it's actually mutable" variables and references is at least as bad as any of the downsides have layed out for the alternative(s) in the above discussion.

2 Likes

You really do. You can't mutate a &RefCell<T> without first acquiring the lock, which would give you &mut T. The fact that this transformation performs runtime checks and happens in unsafe code doesn't mean that &RefCell<T> is suddenly a mutable reference. It isn't, it's something that could be used to acquire a mutable reference.

A shared mutable reference is something which is shared and allows unrestricted mutation in safe code. Such a thing is incompatible with Rust's safety guarantees, in both multithreaded and single-threaded code.

Also not true. The exclusive immutable reference which were used in closures until recently didn't have any relation to &mut. Box<T> is (or at least recently was) treated as a unique pointer, and thus &Box<T> is an exclusive immutable pointer.

There could also be added other reference types, like owning references &move T and write-only references to uninitialized data &uninit T. Both of those would be exclusive, because safe writes are possible only through exclusive references.

I totally see what you mean. This way of thinking is actually where the phrase "interior mutability" originates from. The outer object (the mutex) is immutable, but it has a thing inside it (the value) that is mutable. The interior of the immutable object is mutable.

One possible source of confusion is that the value inside a mutex is not stored behind a pointer. It's stored right next to the low-level mutex that synchronizes the access. This means that your immutable reference to a Mutex<T> directly points to the mutable T value.

I don't think we should actually rename references to shared/exclusive. It's a tradeoff.

  • The current names are easier to explain and fit closer to people's intuition in the normal case.
  • The shared/exclusive names generally explain edge cases a lot better than mutable/immutable names are able to.
3 Likes

Just to be clear, in case I was too ambiguous. I wasn't discussing changing anything about existing Rust, but imagining “what if it was named differently in the first place when Rust was first created?”

There might not actually have been a misunderstanding, since your answer fits this interpretation, but I wanted to be safe, and also clear to other readers as well.

The shared-exclusive was never an accurate description of & and &mut, and it becomes less accurate every day. &mut !Unpin is currently treated as non-exclusive, even though it's mutable. Similarly, the pointers to heap-allocated data (Box<T>, Vec<T>) should be unique since otherwise you couldn't safely write through them and safely deallocate the buffer. But on the other hand, people expect heap-allocated data to have stable memory addresses and to be able to use it, e.g. create a pointer *mut T inside of Vec<T>'s buffer, and have it remaining valid even as Vec<T> moves.

Exclusivity is a requirement to perform writes. That one is a given, otherwise you'd have data races. But everything else is incidental.

1 Like

I would phrase that a bit differently: "You can't mutate a &RefCell but you can mutate the thing it contains.". Which makes "interior mutability" an obvious term. Obviously you can't mutate the RefCell because there is no mut. Perhaps not so obvious without reading the docs is that acquiring the lock yields a mutable reference.

1 Like

To add to this sentiment: people seem to be regularly confused about reborrows, and sometimes NLL. Presumably this happens, when they have been told explicitly that &mut references are exclusive/unique, and took that restriction slightly too literal. And that's quite understandable, as the actual reference value still exists on the stack while it's reborrowed or after its last use.

1 Like