Unable to assign to mutable reference for function argument

How to change the following function argument that is a mutable reference?

fn f(mut x: &String)
{
    println!("{}", x);
    let c = String::from("c");
    x = &c;
    println!("{}", x);
}
   Compiling rust v0.1.0
error[E0597]: `c` does not live long enough
  --> src\main.rs:10:9
   |
6  | fn f(mut x: &String)
   |             - let's call the lifetime of this reference `'1`
...
10 |     x = &c;
   |     ----^^
   |     |   |
   |     |   borrowed value does not live long enough
   |     assignment requires that `c` is borrowed for `'1`
11 |     println!("{}", x);
12 | }
   | - `c` dropped here while still borrowed

The following is fine.

fn f2(mut x: &String)
{
    let mut y = x;
    println!("{}", y);
    let c = String::from("c");
    y = &c;
    println!("{}", y);
}

The reference x holds has to be valid for the whole function call, but c will drop before the function call ends.

In the second version y and c go out of scope at the same time, so it's fine.

To elaborate a bit, this is the interaction of a few properties:

  • You can't borrow local variables for longer than the function body
  • The only thing you know about the lifetime of x is that it is longer than your function body (that it is valid for the entirety of your function)
  • Types are static and that includes their lifetimes

So you can't create a borrow of c long enough to assign to x, as the lifetime of x -- which is part of its type and cannot change -- is longer than the function body, but c is a local variable.

When you create a new variable y, it's inferred to have a lifetime shorter than the function body. The assignment is sort of like casting the value of x to a type with a shorter lifetime.

3 Likes

This reminded me of a quirk or bug [1] wherein subpatterns and non-trivial patterns effectively create a local assignment. So here's a workaround that will confuse others and may even some day not compile:

fn f2(mut x @ _: &String) {
    println!("{}", x);
    let c = String::from("c");
    x = &c;
    println!("{}", x);
}

(Or maybe your OP will start to compile.)


  1. I know I've seen this discussed before 2021; does anyone have a better citation? ↩︎

1 Like

I would like to note that there are no mutable references but only shared references of different lifetimes involved (please correct me if I'm wrong). The mut statement in the function declaration refers to the mutability of the binding. Thus while x is of type &String (shared reference!), we may obtain a &mut x (of type &mut &x) which is a mutable reference that allows us to set x to a different shared reference (or we can set x directly).

To demonstrate:

fn f<'a>(mut x: &'a String)
{
    println!("{}", x);
    let mx: &mut &'a String = &mut x;
    // CAUTION: we create a memory leak here:
    let y: &'a String = Box::leak(Box::new(String::from("c")));
    *mx = y;
    println!("{}", x);
}

(Playground)

What happens here?

  • I changed the function signature to explicitly mention the lifetime 'a of the &'a String passed to the function.
  • Due to the binding x being mutable, we can create a mutable reference mx of type &mut &'a String to the &'a String.
  • But how can we create a new &'a String from inside the function's body? We normally can't because 'a is a lifetime that is longer than the function runs. In this example, I "cheat" by doing a memory leak using Box::leak. Instead of storing the new String::from("c") on the stack, I put it in a Box on the heap, and I then leak that box to obtain a reference to the inner value with a lifetime of my choice (I choose 'a).
  • Now both the originally passed reference and my newly created reference have the same lifetime (with the price of creating a memory leak, which you should avoid in most real-world code, of course!).
  • Using the mutable reference (that could be created because the binding is mut), it's possible to change x to be set to a different value of the same type. y is of such a type (&'a String).

The same example without the mutable reference:

fn f<'a>(mut x: &'a String)
{
    println!("{}", x);
    // CAUTION: we create a memory leak here:
    let y: &'a String = Box::leak(Box::new(String::from("c")));
    x = y;
    println!("{}", x);
}

(Playground)

1 Like

I would say this works because y has a different type than x. While x is of type &'a String, the new variable y can be of type &'b String (where 'a and 'b are two different lifetimes).

To reason my guess:

fn f2<'a>(mut x: &'a String)
{
    let mut y: &'_ String = x;
    //let mut y: &'a String = x; // try this!
    println!("{}", y);
    let c = String::from("c");
    y = &c;
    println!("{}", y);
}

(Playground)

If you force y to be of the same type as x, then the example won't compile anymore.

1 Like

Why does c drop there?

How are the lifetimes of different variables in the following two pieces of code different?

fn main() {
    let a = String::from("a");
    f(&a);
    println!("{}", a);
}

fn f(mut x: &String)
{
    println!("{}", x);
    let c = String::from("c");
    x = &c;
    println!("{}", x);
}
fn main() {
    let a = String::from("a");
    {
        // fn f(mut x: &String)
        let mut x: &String = &a;
        {
            println!("{}", x);
            let c = String::from("c");
            x = &c;
            println!("{}", x);
        }
    }
    println!("{}", a);
}

Where?

As far as I know, references (because they are Copy perhaps?) may drop early:

#![allow(unused_variables)]

fn main() {
    let mut x = String::from("Hello");
    let a = &mut x;
    // by default, `a` drops here if it isn't used below
    let b = &mut x;
    //drop(a);
}

(Playground)

Can someone tell me where (in the Rust reference) this is explained? I didn't find it.

From what I understand, references never really get dropped at all; they can only get invalidated when their source is reborrowed in a way incompatible with the prior reference. Invalid references can simply fall out of scope with no issue. I don't know of anywhere where this is explained fully, except in the Stacked Borrows documentation, which can take a while to fully understand.

1 Like

c drops at the end of its scope because that's how destructors work in Rust. Even with NLL, local variables last until the end of their scope, unless that ownership is given away (e.g. if you explicitly drop it).

In the first example, the lifetime on the parameter could be anything so long as it is valid for your entire function body -- which means it is longer than your function body in f. The borrow in the second example is allowed to be shorter than main.

Everything here is nicely nested (the longer the lifetime, the further it is on the right):

fn main() {
    let a = String::from("a");  // a -------------------+
    f( &a                       // borrow ----------+   |
    )                           //                  |   |
    ;                           //  borrow ends ----+   |
    println!("{}", a            // implicit borrow -+   |
    )                           //                  |   |
    ;                           // implicit ends ---+   |
}                               // a drops -------------+

Here, the pre-existing borrow is longer than f, so it's longer than your function. This forces you to try to borrow c for some lifetime beyond where c drops.

// pre-existing string s -------------------------------------+
// pre-existing borrow of s ------------------------------+   |
fn f(mut x: &String)//                                    |   |
{ //                                                      |   |
    println!("{}", x // implicit reborrow ---+            |   |
    )                //                      |            |   |
    ;                // implicit ends -------+            |   |
    // original borrow unused from here ------------------+   |
    let c = String::from("c"); // c -----------------+    :   |
    //                                               |    :   |
    x = &c; // Lifetime of x can't change -----------💥---+   |
    println!("{}",   // implicit reborrow ---+       |    |   |
    x                //                      |       |    |   |
    );               // implicit ends -------+       |    |   |
   // borrow of c unused from here ------------------💥---+   |
   //                                                |    :   |
}  // local c drops at the end of fn ----------------+    :   |
//                                                        :   |
//                                                        :   |
//                                                        V   V

If the lifetimes weren't forced to be the same, it would all work out.

// pre-existing string s -------------------------------------+
// pre-existing borrow of s ------------------------------+   |
fn f(mut x: &String)//                                    |   |
{ //                                                      |   |
    println!("{}", x // implicit reborrow ---+            |   |
    )                //                      |            |   |
    ;                // implicit ends -------+            |   |
    // original borrow unused from here ------------------+   |
    let c = String::from("c"); // c -----------------+    :   |
    //                                               |    :   |
    let x = &c;      // new lifetime ------------+   |    :   |
    println!("{}",   // implicit reborrow ---+   |   |    :   |
    x                //                      |   |   |    :   |
    );               // implicit ends -------+   |   |    :   |
   // borrow of c unused from here --------------+   |    :   |
   //                                                |    :   |
}  // local c drops at the end of fn ----------------+    :   |
//                                                        :   |
//                                                        :   |
//                                                        V   V

In this version, the lifetime can end as soon as all the borrows are unused (before c drops). There is a somewhat subtle point that c can't interfere with the lifetime until c itself is borrowed, so it's okay that c came into existence in the middle of the lifetime.

fn _main() {
    let a = String::from("a");         // a ------------------------+
    {                                  //                           |                 
        let mut x: &String = &a;       // borrow -----------+       |
        {                              //                   |       |
            println!("{}", x           // imp. ---+         |       |
            )                          //         |         |       |
            ;                          // ends ---+         |       |
// original reborrow unused from here ----------------------+       |
                                       //                   :       |
            let c = String::from("c"); // c --------------------+   |
                                       //                   :   |   |
            x = &c;                    // same life --------+   |   |
            println!("{}", x           // imp. ---+         |   |   |
            )                          //         |         |   |   |
            ;                          // ends ---+         |   |   |
        // borrows are unused past this point --------------+   |   |
        } // c drops -------------------------------------------+   |
    }                                  //                           |
    println!("{}", a                   // imp. ---+                 |
    )                                  //         |                 |
    ;                                  // ends ---+                 |
} // a drops -------------------------------------------------------+

The NLL RFC is the closest thing to a reference as far as I'm aware.

1 Like

Let me add a little more -- the key part in the last example is that the borrow inside x could be shorter than c's scope. This means if we want to alter it to fail like fn f did, we should just have to make the lifetime in x get used somewhere after c has dropped:

fn _main() {
    let a = String::from("a");         // a ------------------------+
    {                                  //                           |                 
        let mut x: &String = &a;       // borrow -----------+       |
        {                              //                   |       |
            println!("{}", x           // imp. ---+         |       |
            )                          //         |         |       |
            ;                          // ends ---+         |       |
// original reborrow unused from here ----------------------+       |
                                       //                   :       |
            let c = String::from("c"); // c -------------+  :       |
                                       //                |  :       |
            x = &c;                    // same life ----💥--+       |
            println!("{}", x           // imp. ---+     |   |       |
            )                          //         |     |   |       |
            ;                          // ends ---+     |   |       |
        } // c drops -----------------------------------+   |       |
                                       //                   |       |
        drop(x);                       // last use of x ----+       |
    }                                  //                           |
    println!("{}", a                   // imp. ---+                 |
    )                                  //         |                 |
    ;                                  // ends ---+                 |
} // a drops -------------------------------------------------------+

And sure enough, now we get a similar error about borrowing c for too long.

Great illustrations!

I misread where c got dropped, so I actually have no problems with it. The real problem is somewhere else.

Do you mean that

  1. x in fn f(mut x: &String) behaves differently than let x: &String,
  2. the former x does not even borrow and unborrow the caller's value, and it is something halfway between a local variable and the caller's variable?

Having experience in other programming languages, my two versions of the code seem to be equivalent. How do scope, variables, and functions act differently in Rust? My imagination of the borrows in the first version is

// pre-existing string s -------------------------------------+
// pre-existing borrow of s ------------------------------+   |
fn f(mut x: &String)//                                    |   |
{ //                                                      |   |
    println!("{}", x // implicit reborrow ---+            |   |
    )                //                      |            |   |
    ;                // implicit ends -------+            |   |
    // original borrow unused from here ------------------+   |
    let c = String::from("c"); // c -----------------+        |
    //                                               |        |
    x = &c; // borrow of c ----------------------|   |        |
    println!("{}",   // implicit reborrow ---+   |   |        |
    x                //                      |   |   |        |
    );               // implicit ends -------+   |   |        |
   // borrow of c unused from here --------------|   |        |
   //                                                |        |
}  // local c drops at the end of fn ----------------+        |
//                                                            |
//                                                            |
//                                                            V

Comparing to

there are extra

  1. "Lifetime of x can't change"
  2. vertical dotted line
  3. crossing lines

What do they mean?

Well, no other langauge has lifetimes as far as I know, so it can be hard to draw an exact parallel. But perhaps it will work to talk about types instead.

Rust is strictly typed, and lifetimes parameterize the types they are a part of. So a &'x str is the same type as a &'y str only if 'x and 'y are the time. Rust also has subtyping, but only where lifetimes are involved: 'x is a subtype of 'y only if 'x outlives 'y (spelled 'x: 'y). Or alternatively phrased, if 'x is valid at least everywhere that 'y is valid. When you "shorten the lifetime" of a reference via reborrowing and the like, you are in a sense casting a value of one type to a value of some supertype. Turning a Cat into an Animal or a Circle into a Shape to use some tired examples. What exactly is a subtype of what depends on variance, but I'm not going to get into that here -- we'll just keep talking about references, which are covariant -- just like with the lifetimes themselves, &'x str is a subtype of &'y str if 'x: 'y.

I'll come back to that in a second, but let me start addressing your actual questions.

The x in fn f(mut x: &String) does behave different than let x: &String. The former is pretty much equivalent to fn f<'a>(mut x: &'a String). The lifetime here is a generic parameter. For each call of the function, it will be replaced with some concrete lifetime. The caller of the function determines the concrete lifetime -- the programmer of the function has no say in it beyond stating any bounds that must be satisfied. This is just how like with fn g<T: Display>(t: T), it is the caller of the function that determines the concrete type of T -- it has to meet the bounds (implement Display), but otherwise, any type will do. The only restriction with an anonymous or otherwise unbounded lifetime is that it has to be valid for at least the length of the call -- the entire function body. Or in other words, all the programmer of the function knows is that the lifetime is longer than the function body.

In contrast, with let x: &String, there is no "outside dictator" of what the lifetime is, and the lifetime is not generic. Instead it is one particular, concrete lifetime which the compiler will infer based on how the variable is used. Within a function body, it could be inferred to be shorter than the function body. A generic lifetime parameter on the function body can never be that short.

Now let's get back to types. Like I mentioned before, two types that differ only in lifetime are still two distinct types. And because Rust is statically typed, the type of a variable doesn't change based on what you assign to it. Of particular interest to us, the lifetime of a variable doesn't change depending on what you assign to it. If it could, that would mean variables could change type, but they can't -- because Rust is statically typed.

The subtyping and implicit reborrows and so on, however, can make it seem like the lifetimes of types are dynamic. Like has been covered in this thread, if you add let x = x to the top of the problematic function, it suddenly compiles. The reason is that you're introducing a new variable that's allowed to have a new type -- and the compiler can infer that it needs to have a shorter lifetime in order for the function to compile. So it chooses a supertype of the left hand side -- it choose a shorter lifetime -- and uses that for the type of your local variable.

The answer to your second question is yes -- in fn f(mut x: &String), there is no automatic reborrowing, no implicit coercion to a different type with a shorter lifetime than the caller provided. If there was, it would be like having let x = x; at the top of the function. Maybe Rust will do this some day, but it doesn't today.


In my diagrams, each vertical line represents a type with a particular lifetime (if it has a lifetime) or the liveness scope before dropped (for variables of types without lifetimes). The longer lifetimes/liveness scopes are further to the right. The dotted : parts indicate a portion of code where the variable is "dead" -- the value it holds isn't used anymore. (Sometimes this matters for determining borrow conflicts.)

Generally speaking, borrows conflict when these scopes cross each other, although there are some special exceptions and subtleties. The diagrams are a tool to understand borrow errors. They are an approximation to the analysis the compiler does.

In the diagram of mine which you quote, when you assign x = &c, x still has to have the same type -- the "lifetime of x can't change" -- and that lifetime is all the way outside of the liveness scope of c. The lifetime of the borrow -- x's lifetime -- is not nested within the liveness scope of c. In the diagram, this makes some lines cross, and corresponds to the borrow error that you get. As far as the compiler's analysys goes, you're trying to borrow c for longer than it's alive, and that's an error.

In your version, the diagram you wrote corresponds to one of my earlier ones, where this change was made:

-    x = &c;
+    let x = &c;

This new x can have its own distinct type, and a locally inferred lifetime that's not the same as the function parameter. It's inferred to be a shorter lifetime than c's liveness scope, because that's what lets the function compile. The borrow can then be comfortably nested within c's liveness scope. You're not trying to borrow c for longer than it's alive, so everything works out.

If the lifetimes of variables could change when you assigned values to them -- if Rust's types were dynamic over lifetimes, if they could shrink based on the value assigned -- then it would work like your diagram. But they can't change, so it works more like my diagram, and you get the borrow error.


Here's some more reading if you're interested:

In summary, lifetime is part of the function signature. Rust automatically annotates it in even when it is omitted. The current Rust does not create a local variable for each function argument so there is no lifetime inference magic. Rust only allows assignments of function arguments that satisfy the annotated lifetime.

Also explained in Lifetime Elision,

After writing a lot of Rust code, the Rust team found that Rust programmers were entering the same lifetime annotations over and over in particular situations. These situations were predictable and followed a few deterministic patterns. The developers programmed these patterns into the compiler’s code so the borrow checker could infer the lifetimes in these situations and wouldn’t need explicit annotations.

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.