error[E0596]: cannot borrow data in a `&` reference as mutable

Hello

I am very new to Rust and come from a C++ background.

I have the following code:


fn change_str(sentence: &mut str) -> bool{

    true
}

fn change_string(sentence: &mut String) -> bool{


    true
}

fn main() {

    let mut s = "Hello, Rust";
    change_str(&mut s);

    let mut s2 = String::from("Hello, Rust");
    change_string(&mut s2);
}

which gives the following error:

error[E0596]: cannot borrow data in a `&` reference as mutable
  --> src\main.rs:16:16
   |
16 |     change_str(&mut s);
   |                ^^^^^^ cannot borrow as mutable

I am quite confused by this error. The linked error indicates the error is when you try to create a mutable reference to a non-mutable variable (I think in C++ this is equivalent to having a non-const reference to a const object) but I cannot see that this is occurring in this case.

Also, why does the same error not occur with the change_string function?

I have read the Rust book section on ownership and borrowing but obviously I must have misunderstood something.

Many thanks

1 Like

String literals ( "Hello, Rust") are immutable. The type of s is actually &str, which means the reference you're passing to change_str is &mut &str. Because of some function call magic, the compiler tries to turn this into &mut str but can't, because the inner part is immutable.

Turning it into a String is the right thing to do. You can pass that to change_str even, since String dereferences to str.

There are ways to make &mut str without creating a String, but they're not very useful.

2 Likes

Ah, many thanks! Makes complete sense.

I am still at that frustrating stage where I am learning to walk. It takes me 30 minutes to do something in Rust which I wouldn't even have to think about if doing in C++!

1 Like

That's probably because you know that string literals in C++ are not const. But they are non-const simply because of compatibility with C, otherwise it would make no sense to turn them non-const-but-don't-try-to-change-them type.

In Rust string literals are immutable and even if you put reference to string literal into mutable string you can not turn them into mutable type.

Because string in Rust is mutable, just like in C++.

I think your issue is that you are trying to perceive &mut as C++'s non-const. But that's very different concept in Rust. &mut in Rust is about uniquiness, not about mutability.

If your reference is shared it may never become “mutable, exclusive” reference. And “mutable, exclusive” reference to str are not much useful (even if it may, technically, exist, it can not be created from string literal).

1 Like

I can well believe that.

However if one does such things in C or C++ one stands a high chance of creating random crashes or erroneous results that are often hard to track down. Which you will then have to spend time thinking about and debugging.

C and C++ are subject to the same borrowing and lifetime rules as Rust, except they don't show up as compiler errors but as run time failures.

Don't worry, it'll pass. It's rough sailing in the beginning but eventually you reach the state where you look on Rust's compiler error message and think about it “hey, compiler have just stopped me from writing code which I would have debugged for days in C++”.

Even extra-advanced C++ programmers sometimes mix up and forget iterator invalidation rules in C++… and let's not forget about all these rules you have to deal with std::move and std::string_view. Ownership and borrow system in Rust is designed precisely and exactly to prevent just this kinds of issues.

At some point you realize just how often the code you wrote in C++ was actually wrong (and just worked because of happy accident and/or because compiler wasn't trying to hard to destroy optimize your program). They you become a Rust fanboy.

3 Likes

If they result in the immediate run-time failures then you are lucky. More frustrating is the case of “are you kidding me? that's 30 years old code!”… with typical follow-up of “yeah, sure, it was broken for 30 years but have have only just found recently how to destroy optimize it”.

In Rust these things are, basically, contained in the unsafe code and even there you have tools too minimize such surprises.

Actually, this example is not very different between Rust and C++. In C++, a string literal still has type const char[], which you can't change, and if you want to append to or otherwise modify a string in place, you have to construct a std::string from it. The exact same thing applies to Rust at the high level, it's just that the specific types are spelled differently.

2 Likes

You can not change it's type, but you can still pass it into a function that only accepts char*. And before C++11 that conversion was implicit!

As long as you don't actually modify it you are fine (well, if you are using clang/gcc/msvcintel compiler just miscompiles valid program in such cases).

Rust have rules which are saner… and as added bonus you don't have to remember which compiler miscompiles which valid constructs.

2 Likes

C++ doesn't have Rust's mutability-xor-shared borrowing rule, which is why code like this is common, and forgetting to check can be catastrophic.

struct Foo {
    Foo& operator=(const Foo& src) {
        // src may legally alias *this
        if (&src != this) {
            // ...
        }
        return *this;
    }
};

Sidenote: &mut str is rarely useful since it's hard to retain UTF-8 constraints.

2 Likes

Very true. I have plenty of experience of far. from immediate run-rime failures due to such errors in C, C++ and languages like Pascal and PL/M.

It does not even require advances in optimisers. Just trying to move code to a different CPU architecture often shows up such mistakes.