1

A beginner of rust. After I read chapter 4.3, I have confusion about the content of chapter 4.3 which has a cross-reference to the principle

At any given time, you can have either one mutable reference or any number of immutable references.

The simplified example is

fn main() { let mut str: String = String::from("hello"); let slice: &str = &str[0..2]; // #1 str.clear(); // #2 println!("{}", slice); } 

This example results in an error when compiling it.

error[E0502]: cannot borrow `str` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 | 3 | let slice: &str = &str[0..2]; // #1 | --- immutable borrow occurs here 4 | str.clear(); // #2 | ^^^^^^^^^^^ mutable borrow occurs here 5 | println!("{}", slice); | ----- immutable borrow later used here 

The tutorial annotation says the reason is that it violates the principle above. However, I cannot understand it. In my mind, #1 creates an immutable reference with the type &str, instead, #2 makes a mutable reference with type &String, according to the type, they seem not to refer to the same things, since they have different reference types. Why does it violate the principle above that seems to only apply to the reference with the same type? Is there any principle that can clarify this issue?

13
  • Does this answer your question? What are the differences between Rust's String and str? Commented Jul 25, 2022 at 9:25
  • A &str is a slice, it can be a pointer to a area inside a String. They share memory so you can't modify the String Commented Jul 25, 2022 at 9:29
  • @mousetail Is there any principle explicitly interpret like this? That is, what is the exact meaning of there cannot exist mutable and immutable reference at the same time? Commented Jul 25, 2022 at 9:31
  • 1
    Right, the Deref trait allows implicit dereferencing. Since the standard library has impl Deref<str> for String, any *T or &T will call the deref method for you, aka "deref coercion". It isn't usually something that you will write in there yourself. I still think it is the criteria you are looking for, maybe that link will explain a little better than the docs. Commented Jul 25, 2022 at 13:33
  • 1
    "Why does it violate the principle above that seems to only apply to the reference with the same type?" - the borrow rules apply regardless of the referenced type, only where it comes from matters. You can borrow the name, &str, from a Person and while you are borrowing it, you cannot modify that person. Commented Jul 25, 2022 at 13:56

2 Answers 2

2

I think you misunderstand.

String is not the mutable version of str. It's its own type.

let mut x: String is the mutable version of let x: String.

String is owned and can be modified. str is a "slice" type and refers to the content of a string, either inside of a String, or as &'static str in the global memory.

There is no mut str because str by definition is a reference to an immutable part of a string.


Let's look at your code. (renamed str to s because this got too confusing)

fn main() { // Your variable `s` is `mut String`. It is a mutable string. let mut s: String = String::from("hello"); // Your variable `slice` is a `&str`. // It isn't mutable, it is a reference to a substring of `s`. let slice: &str = &s[0..2]; // #1 // Here we already hold an immutable reference to `s` through the `slice` variable. // This prevents us from modifying `s`, because you cannot reference an object mutably while // it is borrowed immutably. s.clear(); // #2 // This line is only important to force the variable `slice` to exist. // Otherwise the compiler would be allowed to drop it before the `s.clear()` call, // and everything would compile fine. println!("{}", slice); } 

There is no &String in there anywhere. Taking a slice of a String via &s[0..2] automatically creates a &str instead, because that's what the specification of String says:

fn index(&self, index: Range) -> &str


Why does it violate the principle above that seems to only apply to the reference with the same type?

This is incorrect. They do not have to be the same type. If you hold a &str that references the content of a String, then the String object is also blocked from being mutated while the &str reference exists. You can even store references in other objects and then the existance of those objects still block the original String.


They are definitely different objects

This doesn't mean that they can't be connected.

To demonstrate that two objects of different types can have connected lifetimes, look at the following code:

#[derive(Debug)] struct A { pub value: u32, } #[derive(Debug)] struct B<'a> { pub reference: &'a u32, } impl A { pub fn new(value: u32) -> Self { Self { value } } pub fn set(&mut self, value: u32) { self.value = value; } } impl<'a> B<'a> { pub fn new(a: &'a A) -> Self { Self { reference: &a.value, } } } fn main() { let mut a = A::new(69); println!("a: {:?}", a); // Can be modified a.set(42); println!("a: {:?}", a); // Create a B object that references the content of `a` let b = B::new(&a); println!("b: {:?}", b); // While `b exists, it borrows a part of `a` (indicated through the fact that it has a lifetime type attached) // That means, while `b` exists, `a` cannot be modified a.set(420); // FAILS // This ensures that `b` actually still exists println!("b: {:?}", b); } 

The error message is quite clear:

error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable --> src/main.rs:43:5 | 38 | let b = B::new(&a); | -- immutable borrow occurs here ... 43 | a.set(420); // FAILS | ^^^^^^^^^^ mutable borrow occurs here ... 46 | println!("b: {:?}", b); | - immutable borrow later used here 

Note that the B type has a lifetime 'a attached. This lifetime will automatically be derived by the compiler upon instantiation and is used to prevent mutable usage of the referenced A object for as long as B exists.

&str also has a lifetime attached that is used to prevent mutable access of the referenced String object.

Sign up to request clarification or add additional context in comments.

14 Comments

Thanks for your answers. I just kind of cannot understand: "If you hold a &str that references the content of a String, then the String object is also blocked from being mutated while the &str reference exists." In my mind, from the perspective of c++, slice is a reference to the object created in the heap while the implicitly created &s for calling the method clear is a reference to the object associated with s, why can they consider violate the principle that: "you cannot reference an object mutably while it is borrowed immutable."? they are definitely different objects.
"They are definitely different objects" - objects in Rust can contain references to other objects (even of different types). If so, then their lifetimes are connected, and the referenced original object is locked via the borrow checker. Note that this is a compile time check, nothing is actually locked at runtime.
slice does not copy its data, it just references the data from s. That's why they are connected.
@xmh0511 Added another code example to clarify that objects don't have to be of the same type for lifetimes to work.
"slice is a reference to the object created in the heap" - That is true, but doesn't make the problem go away. It's a question of ownership. The "object created on the heap" is not reference counted, and therefore someone has to own it. The object owning it is s, and that is why s is getting blocked as long as slice exists. I mean, just from a logical point of view: if s.clear() would succeed, then the content of slice would change. And the fact that slice is an immutable reference means that it will not change while slice exists.
|
1

You can think of a String in Rust as holding three pieces of data - a pointer (to an allocated block of memory [on the heap] that holds a contiguous sequence of bytes - essentially a heap-allocated array of u8's), an integer that stores the capacity of the aforementioned block of memory (i.e. the size of the buffer), and an integer that stores the the size of the String (i.e. how much of the buffer is actually being used).

When you create a slice (a &str object) from a String, the slice will still be pointing to the data held by the String object. For all intents and purposes, the data comprising a slice is a const (using C-base parlance) pointer and an integer that indicates the immediate size of the slice (it supplies you NO information about the size of the underlying buffer). In the case of the original post ... the slice variable is referring to the data held by str (as an immutable borrow).

If you then look at the signature-line of the clear method for String objects ...

pub fn clear(&mut self) 

You see that a method-call to clear involves a mutable reference of the calling-object. Thus, once you call the clear method, any access you may have had to the data via slice vanishes. The mutable reference from the method-call causes there to be a mutable-borrow. Your slice variable no-longer is borrowing the data. That's why the Rust compiler throws the error at you, as a result of your println! call.

Even in C or C++, your program would be a bad move, because you're trying to access data that you've just cleared. That could maybe be akin to accessing freed memory with a dangling pointer. These are among the many sort of memory errors that Rust's data-ownership/data-borrowing model attempts to prevent.

fn main() { let mut str: String = String::from("hello"); let slice: &str = &str[0..2]; println!("{}", slice); // prints he str.clear(); // slice is of no use after this line! str.insert_str(0,"world"); println!("{}", str); // prints world } 

The code above compiles and runs without fault. However, it's important to realize that slice only is effectively borrowing the data through the first call to println!. After that, there is a transient mutable-borrow due to the call to clear and then ownership returns to str.

It's important to remember that you can have as many immutable references to an object as you like ... however, once you have a mutable borrow (a mutable reference), then all your immutable borrows are forfeit (you cannot use them again).

Of course, nothing stops you from creating new borrows!

fn main() { let mut str: String = String::from("hello"); let slice: &str = &str[0..2]; println!("{}", slice); // prints he str.clear(); // slice is of no use after this line! str.insert_str(0,"world"); println!("{}", str); // prints world let slice: &str = &str[0..2]; // new immutable borrow! println!("{}", slice); // prints wo } 

Thus, as you can see, this motivates the whole discussion about the lifetimes of references (lifetimes of borrows) ... because, as you observed, they do not live indefinitely.

To address the question, why are &str and &String treated as references to the same value - the answer there is that they are NOT. They both CAN hold pointers to the same data-array (i.e. they can have a data-member in common, which is a pointer). However, in principle, the rest of their data is independent.

Also, you can define &str variables locally that are assigned primitive string-literals. These variables will exist purely locally on the stack. They provide one with means of doing many common string tasks with handy immutable data - without having to use any String object machinery. However, whenever you want that data to persist beyond the stack or you want to be able to mutate the data - then you enter the territory where String objects are particularly useful.

Anyhow, at the end of the day, &str objects serve as immutable, lightweight objects that are quite useful. Furthermore, since they are lightweight and flexible, they are a great way of handling immutable references to String objects too.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.