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?

E_net4
  • 27,810
  • 13
  • 101
  • 139
xmh0511
  • 7,010
  • 1
  • 9
  • 36
  • Does this answer your question? [What are the differences between Rust's `String` and `str`?](https://stackoverflow.com/questions/24158114/what-are-the-differences-between-rusts-string-and-str) – Chayim Friedman Jul 25 '22 at 09: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` – mousetail Jul 25 '22 at 09: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? – xmh0511 Jul 25 '22 at 09:31
  • @ChayimFriedman They are different issues, I think. In this issue, I try to ask whether two different reference types can violate the principle quoted in the question. – xmh0511 Jul 25 '22 at 09:36
  • Yes, but the description of what `&str` is may help you understand the problem. – Chayim Friedman Jul 25 '22 at 09:37
  • @ChayimFriedman I can understand `&str` is a reference to a piece of data of the string in the heap associated with `String`. What I cannot understand is why they are treated as creating two borrowings for the same variable? – xmh0511 Jul 25 '22 at 09:43
  • From [`std::ops::Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html#more-on-deref-coercion): "Values of type &T are coerced to values of type &U". Is that the part you're looking for? – Jeremy Meadows Jul 25 '22 at 12:14
  • @JeremyMeadows There is no dereference in my code. I'm looking for the relevant wording/criteria in the rust that can interpret such a case. – xmh0511 Jul 25 '22 at 13:08
  • 1
    Right, the `Deref` trait allows *implicit* dereferencing. Since the standard library has `impl Deref for String`, any `*T` or `&T` will call the `deref` method for you, aka "[deref coercion](https://doc.rust-lang.org/book/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods)". 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. – Jeremy Meadows Jul 25 '22 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. – kmdreko Jul 25 '22 at 13:56
  • @kmdreko I just want to know which chapter in Rust document says what you're saying. – xmh0511 Jul 25 '22 at 14:25
  • Btw, it is very confusing to name a variable `str`, especially in the context of your question. – Finomnis Jul 25 '22 at 15:07
  • See also: https://stackoverflow.com/questions/28519997/what-are-rusts-exact-auto-dereferencing-rule – E_net4 Sep 26 '22 at 09:26

1 Answers1

1

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.

E_net4
  • 27,810
  • 13
  • 101
  • 139
Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • 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. – xmh0511 Jul 26 '22 at 02:18
  • *"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. – Finomnis Jul 26 '22 at 06:55
  • `slice` does not copy its data, it just references the data from `s`. That's why they are connected. – Finomnis Jul 26 '22 at 06:58
  • @xmh0511 Added another code example to clarify that objects don't have to be of the same type for lifetimes to work. – Finomnis Jul 26 '22 at 07:17
  • *"`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. – Finomnis Jul 26 '22 at 07:28
  • "Immutable borrow" doesn't just mean that you can't modify the referenced content through the reference, but only that it doesn't change at all while the borrow exist. You can absolutely rely on the fact that the content of an immutable borrow stays constant while you borrow it. With that in mind, it should be obvious why `clear()` cannot be called ... it would change the content of the borrowed string. – Finomnis Jul 26 '22 at 07:30
  • Even further... if your `String` is of length `5` and `slice` borrows the substring `[3..4]`. Then what should `slice` be after you resize the string to something shorter than length `3`? In most other languages, this results in undefined behavior, because the memory `slice` is referencing doesn't exist any more. Rust, by definition, doesn't contain undefined behavior, and therefore doesn't allow modifications while a reference exists. – Finomnis Jul 26 '22 at 07:31
  • The C++ documentation [even says so](https://en.cppreference.com/w/cpp/numeric/valarray/operator_at): *"References become invalid on resize or when the array is destructed."*. In other words, accessing a reference/slice after resize results in undefined behavior. Rusts safety guarantees do not allow anything that could result in undefined behavior. – Finomnis Jul 26 '22 at 07:45
  • Is that meaning, the borrowing rules apply to all parts of a variable even though the part is not an intrinsic part of the variable(i.e. the fields of the variable), such as a field of a variable is a pointer to a heap data, if we created a reference to the data pointed to by the pointer field, we also cannot create a mutable reference to borrow the variable of which the pointer is a field? – xmh0511 Jul 26 '22 at 08:12
  • Just to clarify some wordings here: Normal `safe` Rust doesn't have pointers. Raw pointers are part of `unsafe` Rust. Safe Rust uses References or Smart Pointers like `Box` or `Rc`. And the answer to your question is: yes. If you have a reference to a member of a struct, you cannot borrow the entire struct mutably. Otherwise you could use that mutable reference to the struct to extract a mutable reference to the member, which would contradict the fact that we already have an immutable reference to the member. – Finomnis Jul 26 '22 at 11:04
  • Isn't the underlying implementation of `String` based on `Vec`? This means `Vec` has a pointer to the address of the allocated heap. Based on this situation, why the reference to the data in the heap can forbid the creation of the mutable reference to `s`? I don't figure out this part. – xmh0511 Jul 26 '22 at 14:22
  • What do you think is the data that gets mutated if you create a mutable reference to `s`? The `Vec`. And what data do you think is referenced by the `slice`? Also the `Vec`. Do you see the problem? – Finomnis Jul 26 '22 at 15:27
  • What stuff the slice of `string` ultimately refers to is not `Vec`, instead, by checking the source file, its `Vec -> RawVec -> Unique -> NonNull -> *const T`, that's a pointer. The slice, I supposed, should refer to the data in the heap. Is the reference to heap data also regulated by the rule? – xmh0511 Jul 27 '22 at 08:52
  • Yes. If you don't go into `unsafe`, **everything** is regulated by that rule. Of course at some point it becomes a pointer, because deep down, that's the only thing that exists. The pointer itself doesn't have a lifetime. That's why it is necessary to use `unsafe` to access it directly. But the classes the wrap around the pointer are responsible for adding the appropriate the lifetime management. That is called [*soundness*](https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html). – Finomnis Jul 27 '22 at 10:23