21

I've encountered a confusing error about the use of a mutable and immutable borrow at the same time, after I expect the mutable borrow to end. I've done a lot of research on similar questions (1, 2, 3, 4, 5) which has led me to believe my problem has something to do with lexical lifetimes (though turning on the NLL feature and compiling on nightly doesn't change the result), I just have no idea what; my situation doesn't seem to fit into any of the scenarios of the other questions.

pub enum Chain<'a> {
    Root {
        value: String,
    },
    Child {
        parent: &'a mut Chain<'a>,
    },
}

impl Chain<'_> {
    pub fn get(&self) -> &String {
        match self {
            Chain::Root { ref value } => value,
            Chain::Child { ref parent } => parent.get(),
        }
    }

    pub fn get_mut(&mut self) -> &mut String {
        match self {
            Chain::Root { ref mut value } => value,
            Chain::Child { ref mut parent } => parent.get_mut(),
        }
    }
}

#[test]
fn test() {
    let mut root = Chain::Root { value: "foo".to_string() };

    {
        let mut child = Chain::Child { parent: &mut root };

        *child.get_mut() = "bar".to_string();
    } // I expect child's borrow to go out of scope here

    assert_eq!("bar".to_string(), *root.get());
}

playground

The error is:

error[E0502]: cannot borrow `root` as immutable because it is also borrowed as mutable
  --> example.rs:36:36
   |
31 |         let mut child = Chain::Child { parent: &mut root };
   |                                                --------- mutable borrow occurs here
...
36 |     assert_eq!("bar".to_string(), *root.get());
   |                                    ^^^^
   |                                    |
   |                                    immutable borrow occurs here
   |                                    mutable borrow later used here

I understand why an immutable borrow happens there, but I do not understand how a mutable borrow is used there. How can both be used at the same place? I'm hoping someone can explain what is happening and how I can avoid it.

1

3 Answers 3

27

In short, &'a mut Chain<'a> is extremely limiting and pervasive.

For an immutable reference &T<'a>, the compiler is allowed to shorten the lifetime of 'a when necessary to match other lifetimes or as part of NLL (this is not always the case, it depends on what T is). However, it cannot do so for mutable references &mut T<'a>, otherwise you could assign it a value with a shorter lifetime.

So when the compiler tries to reconcile the lifetimes when the reference and the parameter are linked &'a mut T<'a>, the lifetime of the reference is conceptually expanded to match the lifetime of the parameter. Which essentially means you've created a mutable borrow that will never be released.

Applying that knowledge to your question: creating a reference-based hierarchy is really only possible if the nested values are covariant over their lifetimes. Which excludes:

  • mutable references
  • trait objects
  • structs with interior mutability

Refer to these variations on the playground to see how these don't quite work as expected.

See also:

If you think you need these lifetimes to match due to other compiler errors, you may be creating a "self-referential struct"; see here why that doesn't really work.


For fun, I'll include a case where the Rust standard library does this sort of thing on purpose. The signature of std::thread::scope looks like:

pub fn scope<'env, F, T>(f: F) -> T
where
    F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T

The Scope that is provided to the user-defined function intentionally has its lifetimes tied in a knot to ensure it is only used in intended ways. This is not always the case since structs may be covariant or contravariant over their generic types, but Scope is defined to be invariant. Also, the only function that can be called on it is .spawn() which intentionally takes &'scope self as the self-parameter as well, ensuring that the reference does not have a shorter lifetime than what is given by scope.

Internally, the standard library contains this documentation (source):

Invariance over 'scope, to make sure 'scope cannot shrink, which is necessary for soundness.

Without invariance, this would compile fine but be unsound:

std::thread::scope(|s| {
    s.spawn(|| {
        let a = String::from("abcd");
        s.spawn(|| println!("{a:?}")); // might run after `a` is dropped
    });
});

Even if the lifetime of the reference is invariant with respect to itself, this still avoids many problems above because it uses an immutable reference and interior-mutability. If the parameter to .spawn() required &'scope mut self, then this would not work and run into the same problems above when trying to spawn more than one thread.

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

3 Comments

Thank you. I now understand why it doesn't work. I think the solution I seek is in one of those links you provided, especially the Chain of Responsibility one -- that is almost exactly the pattern I'm trying to implement.
Why is it important for Scope::spawn to ensure that the reference it accepts is no shorter than the 'scope parameter to Scope? It seems like it shouldn't matter how long the reference it accepts is valid for, as long as the function it accepts outlives 'scope, right?
I don't think it is essential for spawn to take a &'scope Scope<…> but fn scope itself passes a &'scope Scope<…> into the closure, so using anything smaller than 'scope as the borrow of Scope in spawn doesn't make it any more general. Also it's essential for Scope to be borrowed for 'scope so you it's ensured the borrow checker can proerly detect use after free errors. (the invariant that this API upholds is that all references live shorter than the Scope)
4

The issue isn't lexical lifetimes, and adding an explicit drop won't change the error. The issue is with the &'a mut Chain<'a>- that forces root to be borrowed for its entire life, rendering it useless after the borrow is dropped. As per the comment below, doing this with lifetimes is basically impossible. I would suggest using a box instead. Changing the struct to

pub enum Chain{
    Root {
        value: String,
    },
    Child {
        parent: Box<Chain>,
    },
}

and adjusting the other methods as necesary. Alternatively, using an Rc<RefCell<Chain>> if you want the original to remain usable without consuming self.

2 Comments

Thank you. I might be able to refactor my design to use Box, but I'm frequently pushing and popping these Child objects at the end of the chain, so it gets harder to think about if it isn't owned by the Root.
@rmeador It might be worth trying the Rc<RefCell<Chain>>. The Rc lets you share ownership and the RefCell lets you enforce RAII dynamically rather than statically. Just be careful of reference cycles if you do that.
1

Here is another example to illustrate the lifetime of mutable references:

struct Interface<'a> {
    manager: &'a mut Manager<'a>
}

impl<'a> Interface<'a> {
    pub fn noop(self) {
        println!("interface consumed");
    }
}

struct Manager<'a> {
    text: &'a str
}

struct List<'a> {
    manager: Manager<'a>,
}

impl<'a> List<'a> {
    pub fn get_interface(&'a mut self) -> Interface {
        Interface {
            manager: &mut self.manager
        }
    }
}

fn main() {
    let mut list = List {
        manager: Manager {
            text: "hello"
        }
    };

    list.get_interface().noop();

    println!("Interface should be dropped here and the borrow released");

    // this fails because inmutable/mutable borrow
    // but Interface should be already dropped here and the borrow released
    use_list(&list);
}

fn use_list(list: &List) {
    println!("{}", list.manager.text);
}

Let's understand what we're constraining here. I recommend reading the official Rust book in its original English version. A lifetime represents the valid interval of a variable from creation to destruction.

  • In function parameter and return value constraints, variables x, y, z with the same lifetime 'a means there exists an interval 'a where x, y, z are all valid. This interval starts at the latest creation point of all variables and ends at the earliest destruction point.

  • Lifetime annotations in struct definitions indicate reference lifetimes, showing that the struct's lifetime must be within the referenced data's lifetime, also determined by finding this interval.

struct ImportantExcerpt<'a,'b,'c,'d,'e> {
    part1: &'a str,
    part2: &'b str,
    part3: &'c str,
    part4: &'d Me<'e>
}
struct Me<'a>{
    part5: &'a mut Me<>,
}

According to the rules, the struct's lifetime is within the intersection of 'a through 'e. The potential lifetime 'd should also be within 'e, due to the constraints of the Me struct. There are no constraints between 'a, 'b, and 'c.

  • The lifetime declared after impl for a struct is actually the lifetime of referenced data in the struct, with no connection to method parameter lifetimes.

Lifetimes also have 3 elision rules. Note the terms input lifetimes and output lifetimes, referring to parameters and return values:

  1. Each reference parameter in a function gets a lifetime parameter.

  2. If there's only one input lifetime parameter, it's assigned to all output lifetime parameters.

  3. The first parameter of a method, unless it's a new method, is generally &self, &mut self, self, Box<self>, Rc<self>, etc. If it's &self or &mut self, its lifetime is assigned to all parameters.

Circular References https://course.rs/compiler/fight-with-compiler/lifetime/too-long1.html

The most unique aspect of this example is that we typically explain lifetimes in functions, structs, and methods separately. But here it's complicated with various constraints. Consider combined approaches like:

impl<'a> List<'a> {
    pub fn get_interface(&'a mut self) -> Interface<'a> {
        // ...
    }
}

In the method &'a mut self, the parameter is actually &'a mut List<'a>. Because methods can be written as functions:

pub fn get_interface(&'a mut List<'a> input) -> Interface<'a> {
    // ...
}

This approach combines struct lifetime constraints with function parameter lifetime constraints. It's called "lifetime binding" or "lifetime entanglement", where the compiler thinks this borrow never ends. It means:

  • The mutable borrow of self will last until 'a ends, not just until the method call ends. After the method ends, self is still borrowed. So throughout the entire lifetime 'a, you cannot borrow this struct instance again.

  • The lifetime of the returned value is bound to self's lifetime, meaning they are valid and invalid simultaneously, even if the Interface object is dropped.

When writing code, follow this principle: avoid reference cycles like &'a mut SomeType<'a>. Change it to:

struct Interface<'b,'a> {
    manager: &'b mut Manager<'a>
}
​
impl<'a> List<'a> {
    pub fn get_interface(&mut self) -> Interface {
        // ...
    }
}

But according to rule 3, the lifetime of &mut self is assigned to Interface. The compiler's inferred annotation would be:

impl<'a> List<'a> {
    pub fn get_interface<'b>(&'b mut self) -> Interface<'b,'b> {
        Interface {
            manager: &mut self.manager
        }
    }
}

However, since the created Interface uses &mut self.manager<'a> with lifetime 'a, the compiler thinks the return value is Interface<'_,'a>. This means the compiler's inference is incorrect, and we need to manually supplement:

impl<'a> List<'a> {
    pub fn get_interface<'b>(&'b mut self) -> Interface<'b,'a> {
        Interface {
            manager: &mut self.manager
        }
    }
}

Final Recommendations

I emphasize again, avoid mutable reference cycles like &mut'a SomeType<'a>, because of how Rust types behave with lifetime parameters (variance):

  1. Immutable references are covariant with respect to lifetimes: meaning longer lifetime references can be "shrunk" to shorter lifetime references.

  2. Mutable references are invariant with respect to lifetimes: meaning no lifetime conversions are allowed, they must match exactly.

Immutable circular references are acceptable. For example, modify struct Interface:

struct Interface<'a> {
    manager: &'a Manager<'a>
}
​
// ... rest of the code with immutable references

Also avoid mutable reference propagation like & SomeType<'a>->AnotherType<'a,'a'>, because you need to manually match lifetimes. Use Rc instead of struggling with this; the performance cost isn't significant.

A third example of mutable circular references: m1's type is &'_ mut S<'a>, but later m1 creates a self-reference, making m1's type &'a mut S<'a>, which violates our rule about avoiding such patterns:

struct S<'a> {
    i: i32,
    r: &'a i32,
}
let mut s = S { i: 0, r: &0 };
let m1 = &mut s;
m1.r = &m1.i;  // s.r now references s.i, creating a self-reference

References: https://stackoverflow.com/a/66253247/18309513 and the answers it mentions.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.