Unable to understand borrowing rules

The return type of "demo1" is immutable, and the parameters of "demo2" are also immutable, I think this is fully consistent with the borrowing rules, why is it reported as an error?
Also, the parameter of "demo1" is variable, but the return type is immutable, when I change the return value of "demo1" to &mut String, the type of "b" will automatically become variable.

Make sure to read this (if you haven't already). Feel free to ask follow up questions if anything isn't clear or you don't see how that article's explanations apply.

The relevant (and linked) section is misconception #9, "downgrading mut refs to shared refs is safe". But the whole thing is also worth reading in general.

10 Likes
fn main() {
    let num =10;
    let mut a = String::from("abc");
    let b = demo1(&mut a,&num);
    demo2(&a);
    println!("{}",b);

}

fn demo1<'a>(s1:&'a mut String, a: &'a i32) -> &'a i32 {
    a
}
fn demo2(x: &String) {

}

I'm not returning a degraded immutable reference here!!! Does that mean that after using a mutable borrow call method, the borrowing rule will be a problem as long as it returns a reference of any type? I can not accept this rule

Rust borrow checker has local reasoning. So you're running into #5 "if it compiles then my lifetime annotations are correct". The function signature you write allows the returned reference to be a result of downgrading the input &mut String in principle, so on the caller-site, that possibility is the assumption the borrow checker has to work with.

Local reasoning for borrow checking is a great and important feature as it allows for API stability (changes in implementation don't lead to compilation error for the caller) and local reasoning is also easier for humans.

The way to fix the error is to change the signature of demo1 to use separate lifetimes for the two arguments s1 and a, e. g.

fn demo1<'a, 'b>(s1:&'b mut String, a: &'a i32) -> &'a i32 {
    a
}

or equivalently

fn demo1<'a>(s1:&mut String, a: &'a i32) -> &'a i32 {
    a
}
4 Likes

Although I have returned an unrelated i32 reference and the variable has been accepted, the compiler thinks I may be returning a degraded reference to "&mut a", which combines the life cycle and causes problems... ...rust learning is really steep

For further illustration: Based on just two principles, namely

  • the concrete (target) types in question should not matter for how the borrow checker operates, and
  • the implementation of a function should not matter for how the borrow-checker operates at the call site

it’s easy to show why the compiler must complain about your code in the way it did.

Here’s how a function with a signature of the same form – demo1<'a>(s1: &'a mut Foo, a: &'a Bar) -> &'a Bar – can truly need the limitations imposed at the call-site.

First, here’s the code:

use rand::random;

use std::cell::Cell;

type Foo = u8; // <- could be basically any type
type Bar = Cell<Foo>;

fn demo1<'a>(s1: &'a mut Foo, a: &'a Bar) -> &'a Bar {
    if random() {
        Cell::from_mut(s1)
    } else {
        a
    }
}

Now, some hints of explanation:
(For a deeper understanding, it might also be helpful to look deeper into what Cell and interior mutability is, if you haven’t come across those concepts already.)

This code example features the interior-mutability type Cell which has a way of construcing &Cell<T> from a &mut T. This produces an “immutable” (in such context often better thought of a “shared”) reference to the Cell, but through this reference, it’s still possible to mutate the original value of type T in a way that would, if aliasing &T references where allowed to be created and live in parallel, could lead to undefined behavior via things like iterator invalidation or data races.

The implementation of the function in the code above is one where the returned &Bar reference could be derived from either of the input arguments, so in this case, the function signature is correctly reflecting the precise constraints that users of the function must follow. Unlike in your code example, where the signature could simply be relaxed to be more permissive. But the implementation shall not matter anyways for borrow-checking the caller. The types Foo and Bar are not String and i32 here, but in my opinion, even if a comparable problem is impossible to create using those concrete type (though the reason why that would be impossible would probably need to be discussed in detail first anyways), borrow-checking would become harder to understand if the target types made a significant difference, and I can also imagine that would be somewhat more tedious because you’d need to define – or at least pay attention to – how custom data types interact with such more complex borrow-checking rules.

2 Likes

You may have intended for them to be unrelated, but in the code that you wrote that is not the case. In the code, you've marked all of the references with 'a which ties them all together and makes them related.

The situation is comparable to

fn func<T>(first: T, second: T) -> T {
    first
}

and then trying to do

func("string", 1234);

Even if the programmer meant for the types of first and second to be unrelated, the signature says that they are supposed to be the same type, so the compiler rejects the code.

5 Likes

Some preliminaries first: &mut is a bit of a misnomer. It doesn't just provide mutability, but it guarantees exclusiveness -- if you hold a &mut, nothing else can observe what is borrowed. And on the flip side, calling & "immutable" is misleading, as Rust has "interior mutability" -- structures that can be mutated even through a &. The Cells mentioned above are one example, Mutexes and many OS primitive wrappers like File are others. For this reason, I prefer:

  • to call &mut an exclusive reference [1]
  • to call & a shared reference
  • to use the term "shared mutability" instead of "interior mutability"

Now, I'll go over the mental model that made me stop struggling against the OP, in case it's also enlightening for you. That is, this one:[2]

fn main() {
    let mut a = "abc".to_string(); // 1
    let b = demo1(&mut a);         // 2
    demo2(&a);                     // 3
    println!("{b}");               // 4
}

// Same as `fn demo1<'a>(x: &'a mut String) -> &'a String { ... }`
fn demo1(x: &mut String) -> &String { x }

fn demo2(_: &str) -> i32 { 1 }

The first piece to understand is that the return from demo1 is not somehow transforming the input &mut and discarding the exclusiveness once returned. Instead it's some shared subborrow obtained through the original exclusive borrow.

Subborrows can only be valid where the original was borrowed; in this case that's reflected by the input and output of demo1 having the same lifetime.

Taken together, this means that in order to return a &String that's valid from line 2 to 4, you have to create an exclusive borrow from 2 to 4 as well. There's no taking the borrow back; there's no way to "degrade" the &mut that you've given away to the function. The function (and overall struct or module logic) can count on that borrow staying exclusive. [3]

And that's pretty much it: if a function API demands creating an exclusive borrow for a given lifetime, you'll be committed to it. It's creating the exclusive borrow that may cause borrow check errors. Whether the return is a & or a &mut or a SomethingElse<'_> doesn't matter.

It would be nice if we could get some sort of calling convention where the borrow does "degrade" to be shared locally, but we don't have it yet.


If we now consider your second example:

fn demo1<'a>(s1:&'a mut String, a: &'a i32) -> &'a i32 {
    a
}

Again it doesn't matter what the return type exactly is, all that matters is that the function API demands an exclusive borrow of the String which lasts as long as the return value will be valid; the fix in this case is to make the input lifetimes distinct.

In general, the function API is the contract: The function body has to work within it and handle all the cases the API says it can handle, and the function caller has to meet all the requirements as well. The contract is enforced even if the function body doesn't exercise it fully, like this version of demo1. That gives the function writer the ability to change the body without breaking any callers.


  1. side note, when you called these "variable" it confused me for a minute ↩︎

  2. incidentally, please post code and errors as text, not screenshots ↩︎

  3. And in practice they do, too. ↩︎

10 Likes

I use a translation software, I'm sorry

1 Like

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.