Mutable borrowing String issues

fn main()
{
    let mut s = String::from("Hello");
    let mut x = &mut s;

    println!("{}", s);
    println!("{}", x);
}

This causes this error.

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
 --> src\main.rs:6:20
  |
4 |     let mut x = &mut s;
  |                 ------ mutable borrow occurs here
5 | 
6 |     println!("{}", s);
  |                    ^ immutable borrow occurs here
7 |     println!("{}", x);
  |                    - mutable borrow later used here

error: aborting due to previous error; 1 warning emitted

When I am printing s how is it doing immutable borrowing? If I comment out line 7, then it works fine.

I am pretty lost with what is going on. First of all on line 6, how is it doing immutable borrowing when it already has String::from("Hello"); data?

println! needs to read s in order to print it, but Rust ensures that when something is mutably borrowed with &mut, no one else can access it, not for reading or writing. The reason removing line 7 fixes the problem is that the Rust compiler understands that if x isn't actually used after line 6, it's okay to drop the mutable borrow and allow s to be used again (I'm not sure how it really works in detail, but that's the general idea).

Rust macros like println! work kind of like advanced text find-and-replace. By using cargo-expand we can see that the println! calls actually look like this after the "find-and-replace" is done.

fn main() {
    let mut s = String::from("Hello");
    let mut x = &mut s;
    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(
            &["", "\n"],
            &match (&s,) {
                (arg0,) => [::core::fmt::ArgumentV1::new(
                    arg0,
                    ::core::fmt::Display::fmt,
                )],
            },
        ));
    };
    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(
            &["", "\n"],
            &match (&x,) {
                (arg0,) => [::core::fmt::ArgumentV1::new(
                    arg0,
                    ::core::fmt::Display::fmt,
                )],
            },
        ));
    };
}

The details aren't that important, but you can see s is indeed borrowed inside it. You can read more about borrowing and macros in the Rust book.

1 Like

Thanks for your response :slight_smile:

I understand writing data to allocated memory block can cause data race but why reading has to be prevented though?


Lets say I have this code:

fn main()
{
    let mut s = String::from("Hello");
    
    {
        let x = &mut s;
        x.push_str(", something");
    }

    println!("{}", s);
}

Now the code above is fine, but as you can see, using x I pushed some extra data into s. When x goes out of scope, then I can use s again. But here is the thing, if the pointer memory address changes as I added data, then how would s know what the new pointer address is?

It’s protection against reading partially-written data. If you’re doing a multi-step update of something, you can make sure nothing uses an intermediate state that is inconsistent. As an example, say you have a data structure like this:

struct Multisort {
     items: Vec<MyType>,
     sorted_by_prop1: Vec<usize>,
     sorted_by_prop2: Vec<usize>,
}

Any update is going to take several steps to keep the three Vecs consistent with each other, and trying to read out of the structure during that process is likely to produce an incorrect result. The restriction against reading from an & reference while an &mut reference is active means that no other locking needs to happen for this case: As long as the update code keeps an &mut Multisort reference during the operation, readers are prevented from seeing the intermediate changes.

There’s an extra layer of indirection here: the push_str call is changing the pointer that’s stored inside s. This ability to change the content of s is why the mut keywords are required.

An Exception

Some types, like RefCell and Mutex allow changing their contents through an & reference instead of an &mut reference. They can do this because they guarantee Rust’s safety requirements through other mechanisms.

That makes sense.

I understand but here is one other confusion.

fn main()

{

    let mut s = String::from("Hello");

    let x = &s;

    s.push_str(", Something"); // error here if I run the println! macro

    println!("{}", x);

}

Why can't the push_str() function also update x's pointer?

References are only "smart" at compile time. At runtime it does nothing more than plain'ol raw pointers. So using references never results unpredictable runtime performance issue.

1 Like

That's asking the wrong question. First of all, String's contents are heap-allocated so nothing needs any updating in this case. But even if the need arose, why should a method on a value be responsible for updating everything that transitively depends on said value? That's a proper data flow nightmare.

Just don't violate the RWLock pattern and you'll be fine. You won't need to do so anyway if you structure your code properly.

1 Like

The smart pointers chapter in the book and The Problem With Single-threaded Shared Mutability blog post from ManishEarth might also be helpful

1 Like

I understand that this can be an issue with multi threaded code. But on a single threaded code why does Rust still disallow you to use s when x has a mutable reference to s?

Disallowing access through other references is the raison d’etre of an &mut reference. At some point, the answer is just “because that’s the way it is.”— programming language design requires lots of tradeoffs and compromises between competing needs; this is one of them that Rust makes.

In particular, Rust’s design often prioritizes the ability to reason about a program locally. As a programmer, if I’m trying to figure out what a program is doing, &mut references make my job much easier by eliminating an entire class of nonlocal behavior that I might have to otherwise investigate.

Another example of this principle at work is the lack of type inference in function signatures: the compiler is capable of doing that, but it might require a programmer to read the internals of a function in order to figure out how to use it.

1 Like

Check out The Problem With Single-threaded Shared Mutability. Note that you're right that your specific example doesn't have problems listed in the article, but it'd be hard to implement borrow checker that allows your case but prevents the actually bad things - exceptions are hard. (Although this specific case might work some day, search for "two phase borrows")

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.