Why the borrow checker is ok with `self:&'a` for a struct Foo<'a>

struct Foo<'a> {s: &'a str}

impl<'a> Foo<'a> {
    fn foo(&'a self) -> &'a str {. // <---- this is the line in question
        self.s
    }
}

fn main() {
   let f: Foo<'static> = Foo{s:"123"} ;
   {
       let bf = &f;
       let s = bf.foo();
   }
}

The function foo signature should be fn foo(&self) -> &str {. I don't understand why the borrow checker is ok with fn foo(&'a self) -> &'a str {.

My understanding of lifetime is that they are types (with subtyping and variance). Here self is of type Foo<'static>. foo should not accept any reference with a shorter lifetime than 'static, based on how the code is written. But why bf, a non-static-lifetime reference, can still call foo?

Well, the function is borrow-checked at the definition site. &str is Copy, so you can copy out the &'a str. You don't need 'a on the outer reference:[1]

    fn foo(&self) -> &'a str {
        self.s
    }

But forcing &'a self doesn't cause any problems when it comes to borrow checking the function body, either. The &'a str can be copied out from behind any other reference.

So I guess your question is really about the call site:

   let f = Foo{s:"123"} ;
   {
       let bf = &f;
       let s = bf.foo();
   }

Let me clean this up a little first.

The lifetime on Foo { .. } is probably inferred, so let's get rid of that as I think this is what you are asking about. Also, incidentally, the inner scopes aren't doing anything here. All that happens at the end of the inner scope is that bf and s (shared references) go out of scope, which is almost always a no-op.[2]

   let f = Foo::<'static> {s:"123"} ;
   let bf = &f;
   let s = bf.foo();

And this still compiles.

&'static str is a subtype of &'a str (for any other lifetime 'a), which is why &'static str can coerce to &'a str. Similarly, Foo<'static> is a subtype of Foo<'a>.

More generally, &'long T is a subtype of &'short T.

Additionally, the T in &T is covariant. Which means that if U is a subtype of T, &'x U is a subtype of &'x T. And thus &'longer_than_x U is also a subtype of &'x T.

So for example, both of these assignments can work due to coercing to the supertype:

    // Infer the reference lifetime (unavoidable) and also infer the
    // lifetime that is a parameter on `Foo`
    let bf: &'_ Foo<'_> = &f;
    // Infer the reference lifetime but keep the `Foo` parameter `'static`
    let bf: &'_ Foo<'static> = &f;

And in either case, when you make the call to the foo method -- which requires the lifetimes to be the same -- the parameter on Foo can coerce down to be equal to the reference lifetime.

That is, your &'x Foo<'static> can coerce down to any &'a Foo<'a> where 'x: 'a.

That's why the call succeeds.

Here it is in code form.


  1. and generally you wouldn't want it, but I'll ignore that for the rest of this post ↩︎

  2. It could matter if you had references to references -- &bf or &s -- but you don't. ↩︎

1 Like

Thanks! This is the gist of my question. I understand that &'static str can coerce to &'a str but I thought f/bf's type is "fixed" to be Foo<'static>, that is 'a=='static. But you are saying, when we call foo on bf, it's called on Foo<'x> and not necessarily Foo<'static>. Is that right?

I realized it after saying it out loud. I assume rust should apply type coercion to bf before calling foo.

Yes, that's right.

(Technically it's called on yet another to-be-inferred lifetime, but in practice it is the same as 'x.)


The variance of your own types is inferred. If you want it to be invariant -- so that the lifetime is "fixed" -- you could do so by adding another field where the lifetime is invariant or contravariant.

Example.

1 Like

Thank you!