1
votes

I'm working on the Rust Book (Chapter 4) and I am surprised that code like this compiles:

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

    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2);

    // this line silences the warning: 'variable does not need to be mutable'
    s.push_str(" world");
}

Why does Rust allow an immutable reference to a mutable variable? This would seem to weaken the safety guarantees. If I have a mutable variable, and I pass immutable references to some threads, those threads are assuming the value will not change, but I could mutate the value through the original variable.

I haven't reached threading yet, but found this strange, and in this case, no different from C++:

void doNotChangeMyString(const std::string& myConstString) {
  // ... assume that myConstString cannot change and use it on a thread
  // return immediately even though some worker thread is still
  // using myConstString
}

void main() {
    std::string s = "hello" // not const!
    doNotChangeMyString(s);
    s = "world"; // oops
}

Edit: I fixed the Rust code so that it compiles. Please reconsider the downvotes and close votes. The accepted answer explains a concept that I did not get from Rust Book's chapter on borrowing, was very helpful to me, and could help others who are at the same point in learning Rust.

1
Try to really write the code you're hinting at. You'll see you can't compile it.Denys Séguret
You mean the Rust version? That’s probably the answer, i.e. I haven’t got far enough in the book yet.Matthew James Briggs
Just to be clear, the Rust code you actually posted does not compile, either, so your presumption that Rust allows it is wrong. This is why in the book it's printed with a pink background and a confused crab: to show that it is code that does not compile, for the purpose of illustration.trentcl
The mutable borrow in the Rust code I posted was not necessary for illustrating the question, and you're right, it was a compilation error. I removed the mutable borrow from the example code which should improve the question. The concept I was unaware of, explained by @Optimistic Peach is that the compiler will prevent me from using an immutable reference if the original variable has been mutated. I don't think this was explained in The Rust Book Chapter 4.Matthew James Briggs
"the compiler will prevent me from using an immutable reference if the original variable has been mutated", not quite, you're reading that incorrectly. The NLL (non lexical lifetimes) allow an object's lifetime to be cut short until its last use which is different than not being permitted to use it should the value it points to be mutated. The compiler simply drops the shared reference when you mutate the value. What the compiler does prevent is the mutation of a value when there are live (not implicitly dropped) shared references to it.Optimistic Peach

1 Answers

4
votes

An item's mutability is essentially part of the name of the variable in rust. Take for example this code:

let mut foo = String::new();
let foo = foo;
let mut foo = foo;

foo suddenly becomes immutable, but it does not mean that the first two foos don't exist.

On the other hand, a mutable reference is attached to the lifetime of the object and is therefore type-bound, and will exist for its own lifetime, disallowing any kind of access to the original object if it is not through the reference.

let mut my_string = String::new();
my_string.push_str("This is ok! ");
let foo: &mut String = &mut my_string;
foo.push_str("This goes through the mutable reference, and is therefore ok! ");
my_string.push_str("This is not ok, and will not compile because `foo` still exists");
println!("We use foo here because of non lexical lifetimes: {:?}", foo);

The second call to my_string.push_str will not compile because foo can (in this case it is guaranteed to) be used afterwards.

Your specific question asks something similar to the following, but you don't even need multithreading to test this:

fn immutably_use_value(x: &str) {
    println!("{:?}", x);
}

let mut foo = String::new();
let bar = &foo; //This now has immutable access to the mutable object.
let baz = &foo; //Two points are allowed to observe a value at the same time. (Ignoring `Sync`)
immutably_use_value(bar); //Ok, we can observe it immutably
foo.push_str("Hello world!"); //This would be ok... but we use the immutable references later!
immutably_use_value(baz);

This does not compile. If you could annotate the lifetimes, they'd look something similar to this:

let mut foo = String::new();  //Has lifetime 'foo
let bar: &'foo String = &foo; //Has lifetime 'bar: 'foo
let baz: &'foo String = &foo; //Has lifetime 'baz: 'foo
//On the other hand:
let mut foo = String::new();          //Has lifetime 'foo
let bar: &'foo mut String = &mut foo; //Has lifetime 'bar: mut 'foo
let baz: &'foo mut String = &mut foo; //Error, we cannot have overlapping mutable borrows for the same object!

A few extra notes:

  • Due to NLL (Non Lexical Lifetimes), the following code will compile:

    let mut foo = String::new();
    let bar = &foo;
    foo.push_str("Abc");
    

    Because bar is not used after the mutable use of foo.

  • You mention threading, which has its own constraints and traits involved:

    The Send trait will allow you to give ownership of a variable across a thread.

    The Sync trait will allow you to share a reference to a variable across a thread. This includes mutable references, as long as the original thread does not use the object for the duration of the borrow.

    A few examples:

    • Type T is Send + Sync, it can be sent across threads and be shared between them
    • Type T is !Send + Sync, it can be shared across threads, but not sent between them. An example is a window handle that can only be destroyed on the original thread.
    • Type T is Send + !Sync, it can be sent across threads, but not shared between them. An example is RefCell, which will can only use its runtime borrow-checking on a single thread due to it not using atomics (Multithreading safe components).
    • Type T is !Send + !Sync, it can only live on the thread it was created on. An example is Rc, which cannot send a copy of itself across threads because it cannot count references atomically (Look at Arc to do that) and since it carries no lifetimes to force a single copy of itself to exist when sending across a thread boundary, it therefore cannot be sent across threads.
  • I use &str instead of &String in my third example, this is because String: Deref<str> (You may need to scroll down to see it), and therefore anywhere I need a &str I can chuck a &String in because the compiler will autoderef.