Confusing error in Rust with trait object lifetime
Asked Answered
J

3

8

Can anyone tell what the problem is with the following code? The compiler is complaining about lifetimes, but the error message makes absolutely no sense. I've tried everything I could think of, but nothing seems to help.

use std::borrow::BorrowMut;

trait Trait<'a> {
    fn accept(&mut self, &'a u8);
}

struct Impl<'a>{
    myref: Option<&'a u8>,
}
impl<'a> Trait<'a> for Impl<'a> {
    fn accept(&mut self, inp: &'a u8) { self.myref = Some(inp); }
}

fn new<'a>() -> Box<Trait<'a> + 'a> {
    Box::new(Impl{myref: None})
}

fn user<'a>(obj: &mut Trait<'a>) {}

fn parent<'a>(x: &'a u8) {
    let mut pool = new();
    user(pool.borrow_mut());
}

The compiler error is

error: `pool` does not live long enough
  --> src/wtf.rs:22:10
   |
22 |     user(pool.borrow_mut());
   |          ^^^^ does not live long enough
23 | }
   | - borrowed value dropped before borrower
   |
   = note: values in a scope are dropped in the opposite order they are created

Which makes absolutely no sense. How is the borrower outliving anything? I'm not even using the borrowed value!

Jobless answered 6/9, 2016 at 1:28 Comment(0)
W
11

Ok, this does make sense, but it's hard to see due to lifetime elision. So, here's your code with all the lifetimes written out explicitly, and with irrelevant details culled:

use std::borrow::BorrowMut;

trait Trait<'a> {}

struct Impl<'a> {
    myref: Option<&'a u8>,
}

impl<'a> Trait<'a> for Impl<'a> {}

fn new<'a>() -> Box<Trait<'a> + 'a> {
    Box::new(Impl { myref: None })
}

fn user<'a, 'b>(obj: &'b mut (Trait<'a> + 'b)) {}

fn parent() {
/* 'i: */   let mut pool/*: Box<Trait<'x> + 'x>*/ = new();
/* 'j: */   let pool_ref/*: &'i mut Box<Trait<'x> + 'x>*/ = &mut pool;
            /* BorrowMut<T>::borrow_mut<'d>(&'d mut Self) -> &'d mut T */
/* 'k: */   let pool_borrow/*: &'i mut (Trait<'x> + 'x)*/ = Box::borrow_mut(pool_ref);
            user(pool_borrow);
}

Now, from the perspective of the last line of parent, we can work out the following equivalences by just reading the definition of user and substituting the lifetimes we have in parent:

  • 'a = 'x
  • 'b = 'i
  • 'b = 'x

Furthermore, this lets us conclude that:

  • 'x = 'i

This is the problem. Because of the way you've defined user, you've put yourself in a situation where the lifetime of the pool_ref borrow (which is equal to the lifetime of the pool storage location you're borrowing from) must be the same as the lifetime 'x being used in the thing being stored in pool.

It's a bit like the Box being able to have a pointer to itself before it exists, which doesn't make any sense.

Either way, the fix is simple. Change user to actually have the correct type:

fn user<'a, 'b>(obj: &'b mut (Trait<'a> + 'a)) {}

This matches the type produced by new. Alternately, just don't use borrow_mut:

user(&mut *pool)

This works because it is "re-borrowing". Calling borrow_mut translates the lifetimes more or less directly, but re-borrowing allows the compiler to narrow the borrows to shorter lifetimes. To put it another way, explicitly calling borrow_mut doesn't allow the compiler enough freedom to "fudge" the lifetimes to make them all line up, re-borrowing does.

As a quick aside:

I'm not even using the borrowed value!

Irrelevant. Rust does type- and lifetime-checking entirely locally. It never looks at the body of another function to see what it's doing; it goes on the interface alone. The compiler neither checks, nor cares, what you're doing inside a different function.

Wax answered 6/9, 2016 at 3:30 Comment(4)
Where does the 'b = 'x restriction come from?Jobless
Sorry, I accidentally copy+pasted the already fixed version of the example! To clarify, &'i mut (Trait<'x> + 'x) (the argument) is unified with &'b mut (Trait<'a> + 'b) (the signature of user, before being fixed). This means that 'b is unified with both 'i and 'x.Wax
Oh, that makes sense! I suppose new probably should return something like Trait<'a> + 'b then, but the compiler complained when I didn't have the + 'a in there.Jobless
@Jobless No, I think + 'a is probably more correct: it doesn't seem sensible for the trait object to be bound by two different lifetimes. (In this case, anyway.)Wax
P
4

Note that there's more to the error message:

error: `pool` does not live long enough
  --> src/main.rs:25:10
   |>
25 |>     user(pool.borrow_mut());
   |>          ^^^^
note: reference must be valid for the block at 23:25...
  --> src/main.rs:23:26
   |>
23 |> fn parent<'a>(x: &'a u8) {
   |>                          ^
note: ...but borrowed value is only valid for the block suffix following statement 0 at 24:25
  --> src/main.rs:24:26
   |>
24 |>     let mut pool = new();
   |>                          ^

Let's look at user:

fn user<'a>(obj: &mut Trait<'a>) {}

This says that it will accept a mutable reference (with an unnamed lifetime) to a trait object parameterized with the lifetime 'a.

Turning to new, I'd say the method is highly suspicious:

fn new<'a>() -> Box<Trait<'a> + 'a> {
    Box::new(Impl { myref: None })
}

This says that it will return a boxed trait object with whatever lifetime the caller specifies. That basically never makes sense.

All that said, I'm not clear why the code chooses to use borrow_mut. I would have written that more directly:

user(&mut *pool);

This dereferences the Box<Trait> to get a Trait, then takes a mutable reference, yielding &mut Trait, which compiles.

I cannot currently explain why BorrowMut differs in behavior.

Pollux answered 6/9, 2016 at 1:48 Comment(4)
How do you pass the right lifetime to new in that case? I assumed the compiler would choose the right one automatically. If I try to pass an explicit lifetime in, it gives an error about passing too many lifetime parameters. P.S. I posted the complete error message I saw. I'm not sure why the compiler gives you a longer error message.Jobless
@Jobless you don't pass one in; it's always inferred from the code. In this case there's only a single possible lifetime - 'static so the method should probably be fn new() -> Box<Trait<'static> + 'static> (which I believe is the same as fn new() -> Box<Trait>).Pollux
But that doesn't make sense because the returned object won't actually have static lifetime. Or does it just get implicitly narrowed later on?Jobless
@Jobless that's correct that the returned value won't have the static lifetime, but it can only contain static references. Try to make a smaller example that returns a reference that isn't static and you could se what I mean.Pollux
O
2

I'm not sure why this error happens, but I can give solutions!

First, it seems that using borrow_mut unnecessarily restricts the lifetime of the returned reference. Using operators to create the reference solves the error.

fn parent() {
    let mut pool = new();
    user(&mut *pool);
}

However, if we don't do that, we can solve the error by adding a lifetime bound to the Trait object in user's obj argument.

fn user<'a>(obj: &mut (Trait<'a> + 'a)) {}
Oligopsony answered 6/9, 2016 at 1:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.