#Is this abusing lifetimes? Lifetimes of nested structs containing `&mut` references

41 messages · Page 1 of 1 (latest)

silk tundra
#
struct A;
struct B<'x> { a: &'x mut A}
struct C<'x> { b: &'x mut B<'x>}
struct D<'x> { c: &'x mut C<'x>}


impl<'x> B<'x> {
    pub fn new(a: &'x mut A) -> Self {
        B { a }
    }
}

impl<'x> C<'x> {
    pub fn new(b: &'x mut B<'x>) -> Self {
        C { b }
    }
}

impl<'x> D<'x> {
    pub fn new(c: &'x mut C<'x>) -> Self {
        D { c }
    }
}

fn build_it<F: FnOnce(&mut D)>(a: &mut A, func: F) {
  let mut b = B::new(a);
  let mut c = C::new(&mut b);
  let mut d = D::new(&mut c);
  func(&mut d)

  let _ = d.c.b.a;
}

fn my_func(d: &mut D) {
    let _ = d.c.b.a;
}

fn main() {
    let mut a = A{};
    build_context_and_run(&mut a, my_func);
    //Done!
}

This works, but I've been frequently told that it's bad to write any rust that uses a &'a mut Foo<'a>.

I've built one or two database applications using this pattern to let me separate out different layers of abstraction.

It doesn't feel like it should be valid. How did I get away with this? Is it semantically valid? (honestly, this was just something I tried because I didn't want to write out 8 different lifetimes, and it compiled and worked)

In real life, there are one or two more layers of nesting, parameters, and other cruft that are not shown here.

#

I know from experience that using the above, I absolutely cannot ever explicitly mention a lifetime in a type signature once this 'house of cards' has been built and is being passed around function to function

sudden mantle
#

here, have a simple minimal example showing the issue with &'a mut T<'a>:

#

?play

fn freeze<'a>(_ref: &'a mut &'a i32) {}

let mut a = &42;
{ freeze(&mut a); }
println!("{a}");
#

...if only the bot was working, that is

#

try it in the playground ig

#

even though the mutable reference is created in its own scope, and we don't do anything with it afterwards, the mere act of creating the &'a mut &'a i32 reference effectively locks the a from ever being used again

silk tundra
#

it looks like I manage to avoid ever needing to reuse my toplevel container (struct D)
somehow, moving the toplevel container rather than passing it in by reference seems to let me get away with my ridiculous example

sudden mantle
#

you'd run into issues if you tried to use b again in build_it, even after the func call

silk tundra
#

this seems to work

fn build_it<F: FnOnce(&mut D)>(a: &mut A, func: F) {
  let mut b = B::new(a);
  let mut c = C::new(&mut b);
  let mut d = D::new(&mut c);
  func(&mut d);
  let _ = d.c.b.a;
}

fn my_func(d: &mut D) {
    let _ = d.c.b.a;
}
#

(edited initial example so that others see it too)

sudden mantle
#

note that _ is a non-moving pattern

silk tundra
#

okay, so my example should mutate the contents of the reference to be valid. I can try that

sudden mantle
#

that will likely still be fine

#

what wouldn't work is reading or modifying the b binding

#

since C::new(&mut b) locks b so the binding can never be used again, it's effectively just moving b into C but with more steps

silk tundra
#

(so b and c are inaccessible in build_it, but I can still access them through d)

sudden mantle
#

you basically have a D that owns a C that owns a B, except the ownership is handled through janky mut refs instead of actual owned values

#

it's strictly less useful than just defining struct C<'x>(B<'x>)

silk tundra
#

So I could avoid the mut references entirely in certain situations?
in reality, the types have a bunch of other data

I guess I only really need a reference to the toplevel since it gets constructed exactly once

#

I pass around a lot of mutable references to a/b/c/etc.

sudden mantle
#

i don't think there's any case where it would be possible to create a &'a mut Foo<'a> where you couldn't simply move the Foo<'a> instead

silk tundra
#

so I pay the 'overhead' of moving my data once per layer at construction - which seems totally worth it

sudden mantle
#

Foo<'a> implies that 'a must outlive the Foo, and &'a mut Foo<'a> means you're borrowing the Foo for 'a. so you're borrowing a value for a lifetime that must outlive that same value. in practice this can be resolved by inferring 'a to be exactly the lifetime of Foo, meaning that &'a mut Foo<'a> creates a permanent borrow for the entire lifetime of the Foo value

silk tundra
#

and you've taught me (I didn't realize it) that this is very similar to a move

except maybe it uses a reference instead of physically copying the data

sudden mantle
#

yeah it's effectively equivalent to a move in the sense that a permanent borrow and a move both cause the original binding to be unusable forever

silk tundra
#

but the borrow/reference can be passed around forever - which is maybe why I never noticed. This has been helpful

sudden mantle
#

as long as you don't exit the scope of the original binding, sure (since exiting that scope would cause the value actually being referenced to be dropped)

#

which is another reason why just taking ownership is better, since an owned value can be returned to the parent scope

#

that is to say, this fails:

struct B<'x> { a: &'x mut A}
struct C<'x> { b: &'x mut B<'x>}
struct D<'x> { c: &'x mut C<'x>}
fn build_it(a: &mut A) -> D<'_> {
  let mut b = B::new(a);
  let mut c = C::new(&mut b);
  let mut d = D::new(&mut c);
  d // error: cannot return value referencing local variable `c`
}

but this works:

struct B<'x> { a: &'x mut A}
struct C<'x> { b: B<'x>}
struct D<'x> { c: C<'x>}
fn build_it(a: &mut A) -> D<'_> {
  let mut b = B::new(a);
  let mut c = C::new(b);
  let mut d = D::new(c);
  d
}
silk tundra
#

I actually didn't understand that until you gave the example

#

I think I understand the problem space quite a bit better now

sudden mantle
#

basically, &'a mut Foo<'a> has all the downsides of a move (leaves the original binding unusable), but none of the upsides (can't move a value out of it, and can't return the value to an upper scope)

#

by "can't move a value out of it" i mean that

let new_b = d.c.b;

doesn't work with mut refs (since you can't move out a value via a reference), but it compiles just fine when using owned values

#

so there is never a reason to use &'a mut Foo<'a>, the correct type is either &'b mut Foo<'a> (if you want to avoid freezing the original binding permanently) or simply Foo<'a> if you just want to own the value

silk tundra
#

going from references to moves was a surprisingly easy change in a large (not really large but...) codebase (>10kLOC) - like 5 lines changed and everything else stayed the same

sudden mantle
#

nice

silk tundra
#

Thank you for your help!

sudden mantle
#

incidentally, an easy way to run into the same problem is if you have some code like

struct Foo<'a>(&'a i32);
impl<'a> Foo<'a> {
    fn do_thing(&'a mut self) {
        // ...
    }
}

fn main() {
    let mut a = Foo(&42);
    a.do_thing();
    a.do_thing(); // error: cannot borrow `a` as mutable more than once at a time
}

since in this case, &'a mut self = self: &'a mut Self = self: &'a mut Foo<'a>. so calling a.do_thing() implicitly creates a permanent mutable borrow, locking a and causing the second call to fail

stone condor
kindred hazel