Does passing a mutable reference to a call count as mutably borrowing it again?

(Hi all, I'm still learning Rust and its borrow checking and this may be a beginner's question.)

Why doesn't the following code compile?

struct Node<'a> {
    value: i32,
    next: Option<&'a Node<'a>>,
}

fn bar<'a>(list: &'a mut Node<'a>) {
    list.value += 2;
}

fn foo<'a>(list: &'a mut Node<'a>) {
    for _i in 0..10 {
        bar(list);
    }
    list.value += 1;
}

fn main() {
    let mut list = Node {
        value: 0,
        next: None,
    };
    foo(&mut list);
    println!("{}", list.value);
}

(Playground)

Here are the error messages:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `*list` as mutable more than once at a time
  --> src/main.rs:12:13
   |
10 | fn foo<'a>(list: &'a mut Node<'a>) {
   |        -- lifetime `'a` defined here
11 |     for _i in 0..10 {
12 |         bar(list);
   |         ----^^^^-
   |         |   |
   |         |   `*list` was mutably borrowed here in the previous iteration of the loop
   |         argument requires that `*list` is borrowed for `'a`

error[E0503]: cannot use `list.value` because it was mutably borrowed
  --> src/main.rs:14:5
   |
10 | fn foo<'a>(list: &'a mut Node<'a>) {
   |        -- lifetime `'a` defined here
11 |     for _i in 0..10 {
12 |         bar(list);
   |         ---------
   |         |   |
   |         |   `*list` is borrowed here
   |         argument requires that `*list` is borrowed for `'a`
13 |     }
14 |     list.value += 1;
   |     ^^^^^^^^^^^^^^^ use of borrowed `*list`

error[E0502]: cannot borrow `list.value` as immutable because it is also borrowed as mutable
  --> src/main.rs:23:20
   |
22 |     foo(&mut list);
   |         --------- mutable borrow occurs here
23 |     println!("{}", list.value);
   |                    ^^^^^^^^^^
   |                    |
   |                    immutable borrow occurs here
   |                    mutable borrow later used here
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

Some errors have detailed explanations: E0499, E0502, E0503.
For more information about an error, try `rustc --explain E0499`.
error: could not compile `playground` due to 3 previous errors

Here's what I thought about these errors:

E0499: It says, "A variable was borrowed as mutable more than once", but in foo it seems to me like we are simply passing the already borrowed mutable reference list as a call argument, as opposed to borrowing it again.

E0503: It says, "A value was used after it was mutably borrowed", but likewise it looks like we are simply operating on the already-borrowed mutable reference list, as opposed to using it directly or via another reference.

E0502: It says, "A variable already borrowed as immutable was borrowed as mutable", but it seems to me that the lifetime of the mutable reference &mut list ends at the foo call (line 22) and it's safe to access list.value after it (line 23).

Now how do people reason about this, and how would you fix the code?

if I manually, fully inline foo and bar into main, it compiles. Does passing a mutable reference as a call argument count as mutably borrowing it again? And doing it in a loop is considered as mutably borrowing multiple times?

If I change the type of list from &'a mut Node<'a> to &'a mut Node<'b> in foo and bar, it compiles, which I don't know why.

Thank you!

&'a mut Thing<'a> is an anti-pattern. It means "exclusively borrow Thing<'a> for the rest of it's validity ('a)". Once you create the &'a mut Thing<'a>, you can only ever use Thing<'a> through the &'a mut ever again. You can't call methods on it, you can't move it, you can't print it, and if you have a non-trivial destructor it won't even compile.

So don't require &'a mut on your Node<'a>s.


Incidentally, implementing linked lists in Rust is notoriously hard and a poor way to learn Rust. Doing so with references instead of ownership is probably even more painful. You might want to choose a different project to learn from.

7 Likes

That's mistake number 3 in the list of How not to learn Rust. And I would say this also clearly qualifies for mistake number 7.

He's right. I tried it in the past and wasted a lot of time.

'a, 'b, 'lifetime, 'we_love_the_borrow_checker and similar things are generic lifetime parameters; the Rust compiler will attempt to find a single value for each parameter in order to build the final type[1].

When you write &'a mut Node<'a>, Rust knows two things about this type:

  1. The lifetime parameter on Node is 'a.
  2. The lifetime parameter of the reference is 'a.

As a result, these have to be the same lifetime - which leads to the anti-pattern @quinedot described[2]

In contrast, when you write &'a mut Node<'b>, Rust knows two things about this type:

  1. The lifetime parameter on Node is 'b.
  2. The lifetime parameter of the reference is 'a.

Because they're now different parameters, Rust can choose different lifetimes for the reference, and for the Node, which allows your code to compile. If you need a relationship between the two lifetimes, you can use the 'long: 'short syntax to express that the lifetime 'long outlives the lifetime 'short (equivalently, that 'long is at least as long as 'short, if not longer).


  1. Note that the lifetime components of a type do not affect code generation, only what's accepted as valid Rust by the borrow checker. ↩︎

  2. There's almost certainly an edge case where you really do want a borrow that is guaranteed to last as long as the value is valid, but don't want to pass on ownership, but that's rare. ↩︎

2 Likes

All points noted.

I think that the way it led to &'a mut Thing<'a> for me was:

  1. Use a reference of the type &'a Node<'a> for the next field of Node.
  2. Want to modify the value field, so take a mutable reference of it of type &'a mut Node<'a>.

But it sounds like using &'a mut Node<'b> or &mut Node<'_> for the parameter type for bar and foo was a better idea, as you mentioned.

Yes, I understand that linked lists are hard in Rust and it's not how to learn Rust.

I'm happy to follow that advice. I'm curious to know what to expect though. How do you folks see it now that you have learned Rust? Do you try to avoid using graphs/references as much as possible, or you confidently use them as needed, in everyday coding? What sort of mindset are you in with regard to graphs/references?

I have written some graph-like/pointer-heavy code using Rc<RefCell<T>>, which made sense and went mostly smoothly for me. Now if I try to rewrite it using references/arenas (with lots of lifetime parameters), it gets less sensible for me.

Is there some learning materials about understanding lifetime parameters more deeply or what the borrow checker is internally doing, besides the 'too-many-lists' link, I'd appreciate pointers.

Thanks all!

I still use things like trees and DAGs, but usually avoid parent references (in the sense of a Weak or whatnot) and instead if I need them, have something like a pool of nodes that I can look up by key. A pattern like that I used recently was to have short-lived borrowing traversal structs, something like

struct NodeView<'a> {
    root: &'a Dag,
    node: &'a Node,
}

And the node knew how to ask it's owning Dag for it's parents, etc. [1]

Ultimately the best approach is going to come down to your use case and what you need the data structure for.


I don't know of any one-stop-shop, and a lot of it boils down to experience, but I'll throw some links out:


  1. All the required operations were read-only; it would get trickier if that wasn't the case. ↩︎

1 Like

I can add my own experience to complement @quindot's response.

Absolutely the latter! But we benefit from the ability to use data structures that are already written, tested, and battle hardened in production code. You want a linked list? std::collection::LinkedList. You want a general purpose graph data structure? petgraph. Etc. You probably don't have to write your own data structures. Most experienced Rustaceans wouldn't bother.

References are for temporarily referencing values that are owned elsewhere. That's it. If you try to stretch it more than that to using arbitrarily long lifetimes without very clear lifetime relationships, you are going to have a bad time.

The former solution might be perfectly acceptable in terms of maintenance cost and performance. If you really can gain more of either of these properties with references (maybe performance, but I doubt it would make your code easier to maintain), then you need to be mindful of where the values are actually owned in your graph-like data structures.

With an arena, it's usually just a big blob of memory owned by the root container and all children reference it: Arenas in Rust - In Pursuit of Laziness

Unfortunately, I don't think there is any one resource that will give you an adequate description of lifetimes in Rust without taking you through a whirlwind tour of category theory. My favorite references to return to infrequently are:

1 Like

I still use things like trees and DAGs, but usually avoid parent references (in the sense of a Weak or whatnot) and instead if I need them, have something like a pool of nodes that I can look up by key. A pattern like that I used recently was to have short-lived borrowing traversal structs, something like

In those trees and DAGs, what are the types of the (forward) edges in Node? References or owning pointers (like Box)?

References are for temporarily referencing values that are owned elsewhere. That's it. If you try to stretch it more than that to using arbitrarily long lifetimes without very clear lifetime relationships, you are going to have a bad time.

That sounds insightful.

The former solution might be perfectly acceptable in terms of maintenance cost and performance. If you really can gain more of either of these properties with references (maybe performance, but I doubt it would make your code easier to maintain), then you need to be mindful of where the values are actually owned in your graph-like data structures.

Yeah, mainly performance, and just out of curiosity :slight_smile:

Thanks for all those links! I think I have some reading to do.

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.