Lifetime bound causing error

I ran into a lifetime issue writing some code, and was able to minimize it to the following (Playground):

struct Foo<'a, 'b: 'a, 'c: 'a>(&'a mut &'a (), &'b (), &'c ());
fn with_foo<'b>(_f: impl FnOnce(Foo<'_, 'b, '_>)) {}

struct Bar;
impl Bar {
    fn baz<'a>(&'a self, _foo: Foo<'a, '_, '_>) {}

    fn call_baz<'b>(&'b self) {
        with_foo::<'b>(|foo| self.baz(foo))
    }
}

(Edit: you can further simplify by removing 'b in call_baz(), it still gives the same error)

Rust gives the following error:

error[E0521]: borrowed data escapes outside of method
 --> src/lib.rs:9:30
  |
8 |     fn call_baz<'b>(&'b self) {
  |                 --  -------- `self` is a reference that is only valid in the method body
  |                 |
  |                 lifetime `'b` defined here
9 |         with_foo::<'b>(|foo| self.baz(foo))
  |                              ^^^^^^^^^^^^^
  |                              |
  |                              `self` escapes the method body here
  |                              argument requires that `'b` must outlive `'static`

For more information about this error, try `rustc --explain E0521`.
error: could not compile `playground` (lib) due to 1 previous error

However, editing the definition of Foo to remove the lifetime bound on 'c (ie., replacing 'c: 'a with just 'c) leads to a successful build. I am struggling to understand why adding the bound 'c: 'a causes an error?

Edit: a bound of 'c: 'b also works somehow. Why do 'c and 'c: 'b both build successfully, but NOT c: 'a?

1 Like

I think it's because the closure body has to coerce the input Foo<'a0, 'b0, 'c0> to a Foo<'a1, 'b1, 'c1> where 'a0 = 'a1 = 'b1, implying a 'c1: 'b1 bound that isn't stated elsewhere.

Though I haven't thought of a way that could be exploited...

fn with_foo<'b, 'c>(_f: impl FnOnce(Foo<'_, 'b, 'c>)) {} works as well. And changing 'a to covariant works too.

TBH, I feel like it's a bug and you should file an issue.

with_foo::<'b> is a surprise to me because with_foo should be a late bound while compiler didn't emit an error code

No, argument position impl Trait (APIT) is shorthand for (unnameable) generics.

fn with_foo<'b, 'c, F>(_f: F)
where
    // `'b` and `'c` are part of this where clause (early bound)
    F: FnOnce(Foo<'_, 'b, 'c>)
{}

That said, I did have some cases I fooled around with related to this topic (since deleted) where I could turbofish with an error (some lifetimes were early bound and others weren't, "used to be accepted but is being phased out") where I think the turbofished lifetimes were just ignored (if I made all lifetimes early bound to avoid the warning, a borrow check error resulted).

(Also I don't have a citation for the late/early bound nature of APITs.)

I can't deep dive on it this second, but this topic is definitely beating around the corner cases of the borrow checker. I've encountered some situations in the past where Fn bounds have to be stricter than fn signatures for valid reasons, like stashing data via interior mutabilty. But I haven't figured out how that can be the case for the OP. Could just be a shortcoming of the current borrow check algorithm.

1 Like

Can't it be

fn with_foo<'b, 'c, F>(_f: F)
where
    F: for<'x, 'y, 'z> FnOnce(Foo<'x, 'y, 'z>)
{}

? playground

I guess the original error is caused by code like these:

struct Foo<'a, 'b: 'a, 'c: 'a>(&'a mut &'a (), &'b (), &'c ());
fn with_foo<'b, F>(_f: F)
where
    F: for<'x, 'y> FnOnce(Foo<'x, 'b, 'y>)
{}

struct Bar;
impl Bar {
    fn baz<'a>(&'a self, _foo: Foo<'a, '_, '_>) {}

    fn call_baz<'b>(&'b self) {
        with_foo::<'b>(|foo| self.baz(foo))
    }
}

The explanation:

I assume these 2 rules to be true:

  • early bounds limit HRBT
  • lifetimes in HRBT does not limits each other

so in the code above

F: for<'x, 'y> FnOnce(Foo<'x, 'b, 'y>)

does not means any 'x and 'y, because 'b is early bound and 'b: 'a, so in fact, the code means something like

F: for<'x, 'y> FnOnce(Foo<'x, 'b, 'y>) where 'b: 'x

That is what must keep the code

with_foo::<'b>(|foo| {});

is accepted.

Then consider

with_foo::<'b>(|foo| self.baz(foo))

In concept, it is not a process for a specific 'b, checking whether self.baz(foo) is well formed, on the contrary, it seems a process of checking

baz::<'b>(&'x Bar, Foo<'x, 'b, for<'y>: 'x>) where 'b: 'x

is well formed for &'b Bar.

Because of covariant, &'x Bar always accepts &'b Bar.
But for<'y>: 'x can't always been upheld.

So why the report is

argument requires that 'b must outlive 'static

well, I can't explain this :frowning_face:

Not literally as there's no longer a connection between 'b and 'c and F.

APITs could plausibly result in higher-ranked-over-type-generics function item types, but it'd be a breaking change from today.[1]


  1. One of multiple reasons why it was a mistake to introduce APITs when they did. ↩︎

eh, this may be wrong, because the code accepeted:

struct Foo<'a: 'c, 'b: 'a, 'c>(&'a mut &'a (), &'b (), &'c ());
fn with_foo<'b, F>(_f: F)
where
    F: for<'x, 'y> FnOnce(Foo<'x, 'b, 'y>)
{}

struct Bar;
impl Bar {
    fn baz<'a>(&'a self, _foo: Foo<'a, '_, '_>) {}

    fn call_baz<'b>(&'b self) {
        with_foo(|foo| self.baz(foo))
    }
}

I can't conclude whether or not lifetimes in HRBT limit each other because of the conflict

It looks like the compiler is worried that the type of the closure's input could outlive &'b self (and thus contain a dangling reference). However, passing 'b to with_foo() seems like it should be enough to prove that this wouldn't happen?

It's still unclear to me why adding the bound 'c: 'a causes borrow checking to fail. Probably this is a bug, or just a weird/unintended limitation at the very least?

You can implement the closure manually on nightly. I'm leaning towards it being a bug. Or more accurately, an NLL regression.

I created Error on bounds for unused lifetime · Issue #130014 · rust-lang/rust · GitHub. Thanks to everyone who's taken a look.

1 Like