2

I need some help understanding how to specify lifetimes to make Rust understand what I'm trying to do (or if it's not possible, why not).

First, here's my base case, which works fine. Imagine a factory type Foo which creates Bars, and each Bar borrows the Foo that created it. The purpose of the borrow is so that when a Bar is dropped it can automatically manipulate the Foo that created it. The Foo must obviously outlive the Bar, which is exactly my intent and something that I want the compiler to verify.

struct Foo {
    s: String,
    i: u64,
}

struct Bar<'a> {
    parent: Option<&'a mut Foo>,
    s: String,
    i: u64,
}

impl Foo {
    fn new(s: String) -> Foo {
        Foo { s, i: 0 }
    }

    fn push<'a>(&'a mut self, s: String) -> Bar<'a> {
        Bar { parent: Some(self), s, i: 0 }
    }

    fn print(&self) {
        println!("{}:{}", self.s, self.i)
    }
}

impl<'a> Bar<'a> {
    fn print(&self) {
        println!("{}:{}", self.s, self.i)
    }
}

impl<'a> Drop for Bar<'a> {
    fn drop(&mut self) {
        if let Some(parent) = &mut self.parent {
            parent.i += 1;
        }
    }
}

fn main() {
    let mut x = Foo::new("x".to_string());
    {
        let y = x.push("y".to_string());
        y.print(); // y:0
    } // when y is dropped it increments x.i
    x.print(); // x:1
}

Now what I'm hoping to do is make this pattern recursive so that y could push a new z (where y must outlive z as expected) and so on, recursively. In other words, instead of having separate Foos and Bars, I want to have a single type Foo whose push method creates new Foos which refer back to it. Here's the best that I've come up with after numerous hours of tinkering:

struct Foo<'a, 'b> where 'b: 'a {
    parent: Option<&'a mut Foo<'b, 'b>>, // note 1
    s: String,
    i: u64,
}

impl<'a, 'b> Foo<'a, 'b> {
    fn new(s: String) -> Foo<'a, 'b> { // note 2
        Foo { parent: None, s, i: 0 }
    }

    fn push(&'b mut self, s: String) -> Foo<'a, 'b> { // note 2
        Foo { parent: Some(self), s, i: 0 }
    }

    fn print(&self) {
        println!("{}:{}", self.s, self.i);
    }
}

impl<'a, 'b> Drop for Foo<'a, 'b> {
    fn drop(&mut self) {
        if let Some(parent) = &mut self.parent {
            parent.i += 1;
        }
    }
}

fn main() {
    let mut x = Foo::new("x".to_string());
    {
        let y = x.push("y".to_string()); // error 1
        y.print(); // y:0
    } // when y is dropped it increments x.i
    x.print(); // x:1, error 2
}

Note and error details:

  • Note 1: It feels wrong to use the same lifetime twice in Foo<'b, 'b> because I'm not trying to say that both lifetimes of that Foo need to match. However, using 'a for either one also seems wrong, and I can't specify a new/different lifetime without adding another lifetime parameter to Foo, which in turn forces me to specify a third lifetime here, which leads me to the same problem. Go figure, it's recursive. Just for kicks I checked for a Rust equivalent of C++ variadic templates, but even if that were possible it would still feel like a gross solution.
  • Note 2 These lines smell wrong because I'm using lifetimes in the return type which don't appear elsewhere in the function signature, but I'm not sure what to do about it.
  • Error 1 The error is "borrowed value does not live long enough" and "x dropped here while still borrowed" (where "here" is the very last line). My expectation is that y's reference to x only lives as long as y, but Rust clearly doesn't think that the lifetimes work that way, so it seems as though I haven't done a good enough job of telling the compiler how the lifetimes should be constrained (or not).
  • Error 2 The error here is "cannot borrow x as immutable because it is also borrowed as mutable", which is just another symptom of the same issue. I'm expecting the mutable borrow of x which is inside y to be gone by the time we're calling x.print(), but the compiler isn't.

A lot of what I find when I look for solutions to this issue are about creating circular references, where a parent has a list of children which each refer back to the parent. I understand why that's difficult/impossible, but I don't think that's what I'm trying to do here. My parent/factory types don't maintain references to their children/widgets; the references only go one direction, from the child/widget to the parent/factory.

I've also found myself reading portions of the Rust spec related to this, but I'm quite honestly in over my head there. There's at least one eureka moment regarding how lifetimes actually work that I haven't had yet, and that makes it very difficult for me to follow all of the spec-level details in general, much less as they relate to my problem.

I feel like I want Rust to do exactly what it's good at doing -- verify that the parent thing outlives the child thing -- but I'm not sure how to explain what I'm trying to do to Rust when the parent and child things have the same type. Can anyone help me understand either how to do this or why it can't be done?

cafce25
  • 15,907
  • 4
  • 25
  • 31
Jayonas
  • 401
  • 2
  • 12
  • 4
    The pitfall here I believe is the misuse of reference. In a singly linked list every node should OWN the next node. – xiao Mar 16 '23 at 02:08
  • 4
    Note that this will work fine with _immutable_ references. The problem that you are running into is the _invariance_ of mutable references. – Peter Hall Mar 16 '23 at 02:20
  • 3
    I'm fairly sure that you won't make this work with mutable references. You'll need to use reference-counting instead. – Peter Hall Mar 16 '23 at 02:23
  • 2
    Try using an immutable reference combined with interior mutability (e.g. `Cell`, `RwLock`, etc.). I think you can then just use a single lifetime and rely on covariance? – Solomon Ucko Mar 16 '23 at 02:28
  • 2
    The `self` parameter of `Foo::push` has type `&'b mut Foo<'_, 'b>`. For an explanation about why this is _never_ a good idea, see [this answer](https://stackoverflow.com/a/66253247/5397009) – Jmb Mar 16 '23 at 07:53
  • 1
    I got to this point by such a windy road that I hadn't fully realized that I'd effectively landed on a singly linked list. I also didn't fully appreciate the effect that the reference's mutability was having. While @Jmb's comment doesn't really surprise me on an intuitive level, the provided link led me to a ton of useful detail that will take a while to grok. Lastly, I've realized that I maybe don't need to reference the entire parent, and could instead just reference some of the parent's data (`i`). I feel like exploring all of those insights will probably lead me to a workable solution. – Jayonas Mar 16 '23 at 15:02
  • 2
    For anyone stumbling on this in the future, one of the things I found via @Jmb's link was a Rust book called [Learn Rust With Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/index.html), apparently by the same author as the Rustonomicon. It was great all around, but [Section 8.2: The Stack-Allocated Linked List](https://rust-unofficial.github.io/too-many-lists/infinity-stack-allocated.html) discusses the exact same situation that I stumbled into with this question and provides a good explanation of the relevant complexities. – Jayonas Apr 04 '23 at 17:59

1 Answers1

1

For posterity, I went with the solution suggested by @SolomonUcko in a comment -- using a non-mutable parent reference with interior mutability. Here's the resolved version of the stripped down example in my question:

use std::cell::Cell;

struct Foo<'a> {
    parent: Option<&'a Foo<'a>>,
    s: String,
    i: Cell<u64>, // field manipulated by child uses interior mutability
}

impl<'a> Foo<'a> {
    fn new(s: String) -> Self {
        Foo { parent: None, s, i: Cell::new(0) }
    }

    fn push(&'a self, s: String) -> Self { // non-mut self
        Foo { parent: Some(self), s, i: Cell::new(0) }
    }

    fn print(&self) {
        println!("{}:{}", self.s, self.i.get());
    }
}

impl<'a> Drop for Foo<'a> {
    fn drop(&mut self) {
        if let Some(parent) = &mut self.parent {
            parent.i.replace(parent.i.get() + 1); // mutate parent's Cell contents
        }
    }
}

fn main() {
    let x = Foo::new("x".to_string());
    {
        let y = x.push("y".to_string());
        {
            let z = y.push("z".to_string());
            z.print(); // z:0
        } // z dropped, increments y.i
        y.print(); // y:1
    } // y dropped, increments x.i
    x.print(); // x:1
}

The actual code where I'm using this pattern also has a field of a third-party type that's just a wrapper around an Arc (i.e. ref-counting) and manipulating that from a child also works fine, as suggested by @PeterHall in another comment.

Jayonas
  • 401
  • 2
  • 12