Borrow checker issue with GAT?

While developing a graph library utilizing GATs, I've found myself struggling with the borrow checker on an unexpected occasion.
Stripped down to a minimal example I got

#![feature(generic_associated_types)]

trait Foo {
    type B<'a>: Bar<'a> where Self: 'a;

    fn bar<'a>(&'a self) -> Self::B<'a>;

    fn qux<'a>(&'a self) -> Qux<'a>;
}

trait Bar<'a> {
    fn run(&'a self);
}

struct Qux<'a> {
    phantom: std::marker::PhantomData<&'a ()>
}

impl<'a> Bar<'a> for Qux<'a> {
    fn run(&'a self) {}
}

fn test_bar<F: Foo>(f: F) {
    let b = f.bar();
    b.run();
}

fn test_qux<F: Foo>(f: F) {
    let b = f.qux();
    b.run();
}

Then test_bar contains the compile error (on rustc 1.56.0-nightly (0035d9dce 2021-08-16))

   |
26 |     b.run();
   |     ^ borrowed value does not live long enough
27 | }
   | -
   | |
   | `b` dropped here while still borrowed
   | borrow might be used here, when `b` is dropped and runs the destructor for type `<F as Foo>::B<'_>`

Intuitively, I'd assume that for any implemenation of Bar, the borrowing of b by run ends when run returns.
Why isn't that the case considering that test_qux compiles without problems?

However, I'm not sure if the error is due to an issue with the current GAT implemenation or due to my lack of understanding of lifetimes.

(Playground)

The problem here comes from dropcheck and it is a legitimate error message. The value b of type F::B<'a> does get dropped at the end of test_bar. Yet the run function operates on &'a F::B<'a>. Such a nested duplication of lifetimes very commonly leads to problems. The value is borrowed for the maximum duration that the type could ever exist. In effect, there's no room anymore for any time frame between the shared reference's lifetime ends and the type F::B<'a> itself becomes illegal to use, when a destructor could be run on a fresh mutable borrow of the F::B<'a>. (Note that Drop::drop takes &mut self.)

Two things are different between F::B<'a> and Qux<'a>. The latter doesn't implement Drop and it's covariant with respect to 'a. Dropcheck is less strict when there aren't destructors (i. e. no Drop implementation), and covariance allows turning &'short Qux<'a> into &'short Qux<'short> in order to call run on such a shorter borrow, so there's no need to borrow the Qux<'a> for the full lifetime 'a when it's covariant.

You'll see the same problem with Qux, too, if Qux implements Drop and becomes invariant (only one of these two changes isn't sufficient to create a problem).

3 Likes

If the question is what best to do to fix the problem in your original graph library, we'd probably need to take a look at a less foo-bar-baz-qux-like example and more realistic code.


Also note that it's possible to work around a lack of GATs in stable rust fairly easily, as long as the generic arguments are just lifetimes. So if GATs are the only thing that keeps you on nightly, that's a change to consider.

Here's some examples of how that works

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.