Why can't we have multiple &mut

fn main()
{
    let mut s = String::from("test");

    let a = &s;
    let b = &mut s; // Why is this not allowed?
    
}

I don't understand why is this not allowed? Why can't we have one reference variable (a) that can reference to s and one mutable reference variable (b) that can reference to s?

If the mutable reference is used to change the state of the variable, it can change things out from under the other reference, invalidating assumptions. In your example, if the string is appended to, the whole thing might be copied to a new location with space for the longer string. The pointer underneath reference a is now invalid.

This sort of thing can be done, safely, but it requires more explicit (and potentially more expensive) mechanisms: locks, memory barriers, and other synchronisation primitives. Rust lets us use these mechanisms only when we need them, because the rules for the 'base' case let us have safety without the overhead when we can structure our code within those rules.

5 Likes
fn main()
{
    let mut s = String::from("test");

    //let a = &s;
    let b = &mut s;
    
    b.push_str(" something");

    println!("{}", b);

}

Ok so if you are saying that The pointer underneath reference a is now invalid. if I were to append to b and add more text to it as the location of the memory may have been moved, then how does b know where the new location of the String data is located?

Part of push_str does the reallocation. If it re-allocates, it will update the pointer to point to the new location.

Oh I see,

I got something else to ask, I noticed that variable s is not usable anymore after when I have a reference that points to s, I understand that if I append b to add more text, the location of memory will change, so s's pointer will not update but b will, but why can't Rust also update b's pointer?

In this case the compiler could probably figure it out, but when it gets more complicated, it becomes infeasible to track all the references. There are abstractions to help when you want to have multiple multiple references RefCell. There's also Rc when you want to have multiple owners of some data, and they are often used together.

Then if you want these to work across threads, there are the thread safe versions Mutex and Arc. These are slower though so you use the non-thread-safe ones if you don't need thread safety.

EDIT check out the post below, my answer isn't completely accurate.

2 Likes

@derekdreery that isn't the reason why the compiler doesn't allow it, Rust could handle updating the pointer perfectly fine, because there is nothing to update, i.e.

The String layout is

struct String {
    len: usize,
    cap: usize,
    ptr: *mut u8
}

So the only thing updating during a reallocation is the ptr field on String, the references in main don't need to change at all. The reason why it is wrong to have both a &s and a &mut s, is because when you take a &mut s you are making a promise that there are no other references to s, but that promise is broken when you have both &s and &mut s.

Pretty much all unsafe code relies on the fact that &mut T is a exclusive reference to T, you can even see it in the standard library. In the Mutex documentation for the get_mut method, which says

Since this call borrows the Mutex mutably, no actual locking needs to take place---the mutable borrow statically guarantees no locks exist.

This only works because Rust enforces &mut T to be an exclusive reference. The fact that it is also a mutable reference is incidental is a consequence of this. If you have exclusive access to a value, then you can change it without fear that code will break somewhere else.

For a similar line of reasoning, &T is a shared reference which disallows mutation by default, because that could break seemingly unrelated code. Note that as @derekdreery said there is a way to allow mutation behind a &T, though RefCell, Cell, Mutex, RwLock, because these types have been designed to be safe for shared mutation.

10 Likes

Yes, it's not just the stored pointer that matters. In general, it's any related state, with semantics that are up to the individual implementation.

A good example is if one of the other references had been turned into an iterator. If you were allowed to change the string, the iterator's state becomes invalid: if the string is shortened, it might iterate too many times, or already be off the end of the string; if the string is lengthened, it might not iterate enough times, or might now iterate over deallocated memory.

Rust's lifetime rules are essential not just for knowing when an object is finished with and the storage can be freed, but also for knowing when all the shared immutable references are finished with, so that a new exclusive mutable reference can be allowed safely again.

4 Likes

Does the same apply to C/C++? Does it make the other variables invalid?

Yes, one example is iterator invalidation.

1 Like

This blog post goes about answering this question The Problem With Single-threaded Shared Mutability and in particular it uses many practical examples.

2 Likes

What is a thread exactly?

This stackoverflow question has a nice answer, but if you want a summary, then think of it like a small program that your program starts. The smaller program (Which after this I will refer to as the thread) has access to the rest of the program's resources, but in rust, there are safety rules that guard against the problems with sharing things across threads. Take this example of a race condition:
There is a note on the wall that says the number 12.
Bob walks up to it and takes note of the number 12.
Bob then goes to a secluded room and works out what 12 + 1 is.
While Bob is in the room, Dan comes up and takes note of 12 as well.
While Dan is in another secluded room, Bob comes up and changes the note to say 13.
Dan comes out and then replaces Bob's 13 with the new 13 that had been freshly counted.
The note ends up saying "13" but both Bob and Dan were asked to add 1 to the note.
That's a race condition, and not one that creates undefined behavior, but, this is just one example of a race condition and there are a few more that I won't talk about. A thread, in other words, is just a separate thread of execution that has access to your main thread's resources. A thread in rust is what a regular thread is, and also a smart guard against race conditions and undefined behavior.
(Sorry for the verbose answer, but I don't find threads too easy to explain)

1 Like

Thanks for your explaination :slight_smile:

2 Likes

I got this other question.

image

So if make s point to s1, s is a pointer, right? And s1 is not allowed to become immutable. Why is that though, I don't see a reason how s can become a dangling pointer.

Take a look at this diagram. This is what s and s1 look like, right? So if I append s1's value and the location of the heap changes, therefore the pointer of s1 would update, right? And the s variable is a pointer that points to s1, not directly to the heap of where the String is stored. So the s pointer wouldn't update. So I don't get why Rust does not allow us to not append to s1 when being referenced by another variable?

1 Like

That particular example is fine- you're correct that s can't become dangling. The problem is that in general the compiler can't prove that you haven't used s to produce another pointer to the actual text, which would become dangling. Because you can copy &T pointers all you like (this is why they're called "shared"), the compiler errs on the side of caution and forbids that sort of thing, even though some cases are fine.

There's also a slightly deeper reason to forbid this pattern. The compiler wants to be able to optimize your code to avoid loading and storing through pointers all the time. So anywhere it sees a shared reference, it assumes the thing it points to can't change- for example, it might read something out of s1 and cache it in a CPU register, rather than reading it each time it gets used. Changing s1 would invalidate this caching.

This, again, doesn't apply to this particular program, because you never use s... but because of that, the compiler is actually smart enough to allow this program despite both the above problems: Rust Playground. This is a recent new feature called "NLL," a smarter way to decide whether a reference needs to be kept valid.

6 Likes

I see

How do you use s to produce another pointer to the actual text, may I please have an example?

Oh so is this officially introduced into the compilers and we can download this update, right?

How does it exactly work?

The most common way would be via "deref coercion"- if you have a &String like s and you pass it to a function that takes a &str, it will automatically call the deref method. The resulting &str points directly to the actual text.

Yep! You can see more information about it on Niko Matsakis' blog: Non-lexical lifetimes: introduction · baby steps. There are some later posts that go into more detail about how it works as well.

This is activated by specifying

edition = "2018"

in your Cargo.toml, and is automatically included in a new rust project.

I'd just like to clarify this a bit more, it's just like how a Vec<T> contains a pointer to the data, and can be resized, and the underlying data is accessed with Vec's indirection and is analogous to String, while &[T] is a pointer to the data in the Vec and the [T] represents the data itself (Albeit in a bit of an abstract fashion) and the same applies to &str and str.