[traits, lifetimes] Incredibly vague error message has been bugging me for days

Full code:

enum NativeInstance<'a> {
    A(&'a AInstance)
}

trait Instance {
    fn native(&self) -> NativeInstance;
}

pub struct AInstance;

impl Instance for AInstance {
    fn native(&self) -> NativeInstance {
        NativeInstance::A(self)
    }
}

fn a_instance(instance: &impl Instance) -> &AInstance {
    match instance.native() {
        NativeInstance::A(A) => A,
    }
}

///adapter
enum NativeAdapter<'a> {
    A(&'a AAdapter<'a>),
}

trait Adapter<'a> {
    fn enumerate(instance: &'a impl Instance) -> Vec<Self> where Self: Sized;
    fn native(&self) -> NativeAdapter;
}

pub struct AAdapter<'a> {
    instance: &'a AInstance,
}

impl<'a> Adapter<'a> for AAdapter<'a> {
    fn enumerate(instance: &'a impl Instance) -> Vec<Self> where Self: Sized {
        let instance = match instance.native() {
            NativeInstance::A(instance) => instance,
        };

        ///enumerate...
        let mut e = Vec::new();

        e.push(Self {
            instance
        });

        e
    }
    fn native(&self) -> NativeAdapter {
        NativeAdapter::A(self)
    }
}

trait Device<'a> {
    fn new(adapter: &'a impl Adapter<'a>) -> Self where Self: Sized;
}

struct ADevice<'a> {
    adapter: &'a AAdapter<'a>,
}

impl<'a> Device<'a> for ADevice<'a> {
    fn new(adapter: &'a impl Adapter<'a>) -> Self where Self: Sized {
        let adapter = match adapter.native() {
            NativeAdapter::A(a) => a,
        };

        Self {
            adapter
        }
    }
}

fn adapter_new(instance: & impl Instance) -> impl Adapter {
    AAdapter::enumerate(instance).pop().unwrap()
}

fn device_new<'a>(adapter: &'a impl Adapter<'a>) -> impl Device<'a> {
    ADevice::new(adapter)
}

#[test]
fn main() {
    let a = AInstance;
    let aref = a_instance(&a);

    //let adapter = AAdapter::enumerate(&a).pop().unwrap();

    //let device = ADevice::new(&adapter);

    let adapter = adapter_new(&a);
    let device = device_new(&adapter);
}

Error:

error[E0597]: `adapter` does not live long enough
  --> tests/impl_trait.rs:95:29
   |
95 |     let device = device_new(&adapter);
   |                             ^^^^^^^^ borrowed value does not live long enough
96 | }
   | -
   | |
   | `adapter` dropped here while still borrowed
   | borrow might be used here, when `adapter` is dropped and runs the destructor for type `impl Adapter<'_>`

It's been a long time since I have problems with lifetimes but this one I can't solve for days and I have no idea where the error is, the message is absurdly vague.

Edit:

This snippet compiles:

let adapter = AAdapter::enumerate(&a).pop().unwrap();
let device = ADevice::new(&adapter);

Does not compile

let adapter = adapter_new(&a);
let device = ADevice::new(&adapter);

The problem here is not the bound on the input type parameter. I think the problem is that the impl Adapter<'_> return type of

fn adapter_new(instance: &impl Instance) -> impl Adapter<'_>

is invariant in its lifetime, and also conservatively assumed to - potentially - implement Drop, which together makes it impossible to create a reference to it with the same lifetime.

Minimal example with a concrete type (invariant + implements Drop):

struct Foo<'a>(PhantomData<&'a mut &'a ()>);
impl<'a> Drop for Foo<'a> {
    fn drop(&mut self) {}
}

fn demo() {
    fn expected<'a>(x: &'a Foo<'a>) {}

    expected(&Foo(PhantomData)); // <- fails to compile
}

A reduction:

trait Trait<'lt> {}

fn make<'lt> () -> impl Trait<'lt>
{
    impl Trait<'_> for () {}
}

fn consume<'lt> (_: &'lt impl Trait<'lt>)
{}

fn main ()
{
    let it = make();
    consume(&it);
}

The issue lies in the "nested borrow with identical lifetime" antipattern:

This is saying that you have something involving some borrow 'a, and which you happen to be borrowed for the lifetime of that borrow!

Consider, for instance, the inner lifetime to be 'static: say you have a r: &'static str. You can borrow that r yourself (as &r), for some lifetime duration 'borrow, but it will be almost impossible to have that duration of the nested borrow be (as long as the inner one, that is) 'static:

let r: &'static str = "Hi";
let at_r: &'static &'static str = &r; // Error, can't borrow `r` for `'static`

So you should be using two distinct lifetime for such nested borrows:

let at_r: &'_ &'static str = &r;

and, more generally:

&'r impl Adapter<'a>

or, simply:

&'_ impl Adapter<'a>

since, precisely to avoid the error of repeating lifetime parameters too much, it is advised to use '_ as often as possible / to use explicitly-named lifetime parameters as rarely as possible (as any rule of thumb, this is not a guideline to apply all the time, since there are instances where naming lifetime parameters can improve readability: the main anti-idiom is thus to repeat lifetime parameters unnecessarily).

  • Now, in practice, you'll notice that if you had a &'a &'a str, then you can have &r match that type. This is because of a &'r &'static str is allowed to be "implicitly coerced" / "implicitly casted" / upcasted to &'r &'r str, since 'static : 'r and since "lifetime parameters behind &s (but not &muts!) are allowed to 'shrink'" (we say that &'lt _ is covariant in 'lt).

  • Also note that, even when not in this covariant case, i.e., for some generic-over-a-lifetime (but not covariant) type Inv<'lt>, it can be possible to construct a &'lt Inv<'lt>:

    struct Inv<'lt>(*mut Self);
    
    fn consumes<'lt> (_: &'lt Inv<'lt>)
    {}
    
    let inv = Inv(0 as _);
    {
        consumes(&inv); // OK
    }
    drop(&inv); // <- `inv` mentioned here,
                //     thus `'lt` spans at least until here
                //     thus the `&inv` borrow in `consume` spans at least until here
                //     etc.
    
    // drop(inv);
    

    but this means that inv has now, as far as Rust is concerned, been borrowed for as long as it could possibly live! Indeed, if inv is witnessed / mentioned at any point in the code, then its type must be well-formed at that point, which means that 'lt must span at least until that point.

    This is effectively achieving in Rust something quite rare: a maximal borrow.

    image

    This won't cause trouble as long as nothing were to conflict with that maximal borrow: if one tries to &mut inv, or even drop(inv), e.g., by uncommenting that last line, then by the previous rationale, the borrow of &inv extends beyond that point of usage of inv, and thus we have a shared borrow overlapping with an attempt to get unique access to inv.

    But what of implicit drops? Doesn't Dropping require &mut self i.e., self: &'_ mut Self,i.e., self: &'_ mut Inv<'lt>? Doesn't that count as a region over which 'lt must span, and thus, a region which overlaps with that &'lt Inv<'lt> maximal borrow?

    And the answer is that yes, whenever Inv<'lt> has drop glue that involves that 'lt, for instance, when Inv<'lt> : Drop / when impl<'lt> Drop for Inv<'lt> {, then yes, the implicit drop of inv counts as a call to <Inv<'lt> as Drop>::drop, and thus, as a usage of inv, and one which requires unique / exclusive access, causing a compile error:

    struct Inv<'lt>(*mut Self);
    
    fn consumes<'lt> (_: &'lt Inv<'lt>)
    {}
    
    let inv = Inv(0 as _);
    {
        consumes(&inv); // Error, 
    }
    
    impl<'lt> Drop for Inv<'lt> {
        fn drop (self: &'_ mut Inv<'lt>)
        {}
    }
    

    yields:

      --> src/main.rs:10:18
       |
    10 |         consumes(&inv); // Error
       |                  ^^^^ borrowed value does not live long enough
    ...
    17 | }
       | -
       | |
       | `inv` dropped here while still borrowed
       | borrow might be used here, when `inv` is dropped and runs the `Drop` code
    

    Otherwise, with no drop glue involving 'lt, then thanks to NLL (non-lexical lifetimes), it's not because a local hasn't been drop yet / because a local is still alive, that it's necessary live: life vs. liveness distinction. And that's why the maximal borrows can exist, but only for types with no drop glue (involving 'lt).

And in your case, you had an -> impl Trait… existential: such an existential type could, within a semver-compatible release, be given inherent drop glue / impl Drop even if the current implementation doesn't. That is, -> impl Trait… is considered, by Rust, as -> impl Drop + Trait….

Moreover, if the trait is generic over a lifetime, as in your snippet, then an impl Trait<'lt> existential type also happens to be invariant.


Combine everything, and in that &'lt (impl Trait<'lt>) input you have:

  • A nested borrow with identical lifetime…

  • with the inner type (the impl Trait<'lt> existential[1]) being invariant in 'lt, thus leading to a maximal borrow…

  • and by virtue of being an existential type, it is assumed to impl Drop, thus "NLL can't apply" / implicit drop of such a local counts as a Drop::drop(&mut …) call, which involves a &mut borrow which thus overlaps with the maximal borrow.

Hence the error


  1. Granted, consume, in the minimal repro, device_new in the OP, is featuring an impl Adapter<'a> universal (generic), since it's in impl Trait in input position (IPIT). But it just so happens that the actual choice of that generic type, is the return values of make / of adapter_new, which is an existential type (RPIT). In other words, the impl Adapter<'a> generic input is instanced with a RPIT/existential impl Adapter<'a> :sweat_smile: ↩︎

5 Likes

Given the reply above, I'm not going to write up an explaination, but here's a diff to remove enough lifetime pairing to compile.

 trait Device<'a> {
-    fn new(adapter: &'a impl Adapter<'a>) -> Self where Self: Sized;
+    fn new<'b>(adapter: &'a impl Adapter<'b>) -> Self where Self: Sized;
 }

 impl<'a> Device<'a> for ADevice<'a> {
-    fn new(adapter: &'a impl Adapter<'a>) -> Self where Self: Sized {
+    fn new<'b>(adapter: &'a impl Adapter<'b>) -> Self where Self: Sized {
         let adapter = match adapter.native() {
             NativeAdapter::A(a) => a,
         };

         Self {
             adapter
         }
     }
 }

-fn device_new<'a>(adapter: &'a impl Adapter<'a>) -> impl Device<'a> {
+fn device_new<'a, 'b>(adapter: &'a impl Adapter<'b>) -> impl Device<'a> {
     ADevice::new(adapter)
 }
3 Likes

I will need more knowledge on Rust to absorb all the content, especially related to subtypes, but I understand the problem itself, thank you very much.

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.