Differences in lifetime of method and non-method

Hello. I'm quite new to Rust. I was experimenting with Rust and found this issue where method and non-method result in different in lifetime. I have an assumption on why it work the way it does, but I'm not so sure about it. Please correct me if I'm mistaken, or please elaborate further.

I guess the compiler always assume lifetime 'a of the struct to everything that is borrowed from &self parameter. Resulting in the return type of new_from_self be Container<'a, Item>, which causes problem when I assign it to self.other which mutates self. The compiler thinks I'm keeping immutable reference while mutating the object, which is not allowed.

    struct Container<'a, Item> {
        item: &'a Item,
        other: Option<Box<Container<'a, Item>>>,
    }

    impl<'a, Item> Container<'a, Item> {
        fn new_from_self(&self) -> Container<Item> {
            Container {
                item: self.item,
                other: None,
            }
        }

        fn new_from_self_with_lifetime_bound<'b>(&self) -> Container<'b, Item>
        where
            'a: 'b,
        {
            Container {
                item: self.item,
                other: None,
            }
        }

        fn new_from_non_method(other: &Self) -> Self {
            Container {
                item: other.item,
                other: None,
            }
        }

        fn item_to_other(&mut self) {
            // This doesn't work, got E0506 because I'm assigning it to self.other,
            let new_container = self.new_from_self();
            self.other = Some(Box::new(new_container));

            // this does work
            // let new_container = self.new_from_self_with_lifetime_bound();
            // self.other = Some(Box::new(new_container));

            // this does work
            // I assume the lifetime got resolved into something similar to new_from_self_with_lifetime_bound
            // let new_container = Self::new_from_non_method(self);
            // self.other = Some(Box::new(new_container));
        }
    }

(Playground)

A few things to know here are

Taking those together, the elaborated lifetimes in your playground are:

    impl<'a, Item> Container<'a, Item> {
        fn new_from_self<'s>(
            self: &'s Container<'a, Item>
        ) -> Container<'s, Item> { /* ... */ }

        fn new_from_self_with_lifetime_bound<'b, 's>(
            self: &'s Container<'a, Item>
        ) -> Container<'b, Item>
        where
            'a: 'b,
        { /* ... */ }

        fn new_from_non_method<'o>(
            other: &'o Container<'a, Item>
        ) -> Container<'a, Item> { /* ... */ }

Then in the problematic portion:

        fn item_to_other<'s>(self: &'s mut Container<'a, Item>) {
            let new_container = self.new_from_self();
            self.other = Some(Box::new(new_container));

Lifetimes can't change underneath a &mut (we say they are invariant), so you need to create a Option<Box<Container<'a, Item>>> here. Looking at the signature on new_from_self, you would have to reborrow *self for 'a. But the reference you have to *self doesn't last that long; it only lasts for 's.

new_from_non_method works because the Container<'a, Item> lifetime is the same in the output as in the input due to the Self alias. The reborrow of *self can be arbitrarily short when calling it.

new_from_self_with_lifetime_bound works the same as new_from_non_method (aside from being a method or not) when 'b is equal to 'a. So writing it the following ways also works:

fn new_from_self_with_lifetime_bound(&self) -> Self { /* ... */ }
fn new_from_self_with_lifetime_bound(&self) -> Container<'a, Item> { /* ... */ }

  1. I recommend using #![deny(elided_lifetimes_in_paths)] to make it more obvious when you're returning a borrowed lifetime. It makes things more clear once you've absorbed the elision rules. ↩ī¸Ž

1 Like

Oh, one more thing. Sometimes the compiler gives bad advice, and this scenario is one of those. With the signature written like so, the compiler says:

error: lifetime may not live long enough
  --> src/lib.rs:32:33
   |
8  |     impl<'a, Item> Container<'a, Item> {
   |          -- lifetime `'a` defined here
...
30 |         fn item_to_other<'s>(self: &'s mut Container<'a, Item>) {
   |                          -- lifetime `'s` defined here
31 |             // This doesn't work, got E0506 because I'm assigning it to self.other,
32 |             let new_container = self.new_from_self();
   |                                 ^^^^^^^^^^^^^^^^^^^^ argument requires that `'s` must outlive `'a`
   |
   = help: consider adding the following bound: `'s: 'a`

Now the signature here is

fn item_to_other<'s>(self: &'s mut Container<'a, Item>) {

And when you have a &'s [...] Thing<'a>, it is implied that 'a: 's. The compiler has suggested that we add 's: 'a. Taken together, this means 's must equal 'a, so the net result of applying the help would be that our signature is the same as:

fn item_to_other(self: &'a mut Container<'a, Item>) {

This is an anti-pattern because it requires borrowing the Container<'a, Item> exclusively for the entire rest of it's existence ('a). Once you do this by calling the method, you cannot use the Container directly ever again -- you can't call any more methods, and you can't even move it.

So a &'a mut Thing<'a> is pretty much never what you want. It can be easy to accidentally wind up with it when you write things like so:

// Self is `Container<'a, Item>`
fn item_to_other(&'a mut self) {

See main in this playground for an example.

1 Like