Trait implementations, variance and the borrow checker

I'm confused about how the Rust compiler deals with relaxed lifetime bounds in trait implementations. Here's an example:

trait Foo {
    fn bar(&self) -> &i32;
}

impl Foo for () {
    fn bar(&self) -> &'static i32 {
        &42
    }
}

fn main() {
    let a = {
        let b = ();
        let c: &'static i32 = b.bar();
        c
    };
    dbg!(a);
}

The error is

error[E0597]: `b` does not live long enough
  --> src/main.rs:14:31
   |
12 |     let a = {
   |         - borrow later stored here
13 |         let b = ();
   |             - binding `b` declared here
14 |         let c: &'static i32 = b.bar();
   |                               ^ borrowed value does not live long enough
15 |         c
16 |     };
   |     - `b` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.

(Playground)

I understand that Rust allows the return value of a specific implementation of a trait function to have a longer lifetime than required in the trait definition, since function types are covariant with respect to the return type. The compiler accepts the modified lifetime in the impl, and allows us to assign the return value of ().bar() to a reference with static lifetime, so the compiler clearly understands that the lifetime of the return value of bar() is not coupled to the lifetime of the self reference in this case. Nevertheless the borrow checker appears to keep the borrow of b alive beyond the let statement that calls b.bar().

What is going on here? Is this a bug (or expected limitation) in the borrow checker, or is the code actually unsound?

For what it's worth, the code compiles just fine if we inline b, which certainly won't increase the lifetime of the self reference passed to bar().

I don't think that's true. The 'static check is not part of the type checker. If it's not understanding that it's static that would be a borrow checker error, which is exactly what you get.

While impls can be written more generically, you generally cannot use those refinements in the caller today.

Indeed, if you simplify your example to just

trait Foo {
    fn bar(&self) -> &i32;
}

impl Foo for () {
    fn bar(&self) -> &'static i32 {
        &42
    }
}

fn main() {
    let b = ();
    let c: &'static i32 = b.bar();
}

Then you'll get what you were probably expecting:

error[E0597]: `b` does not live long enough
  --> src/main.rs:13:27
   |
12 |     let b = ();
   |         - binding `b` declared here
13 |     let c: &'static i32 = b.bar();
   |            ------------   ^ borrowed value does not live long enough
   |            |
   |            type annotation requires that `b` is borrowed for `'static`
14 | }
   | - `b` dropped here while still borrowed

So it seems to me like what's happening is just that the compiler decided to show the other error instead of this one, and didn't want to flood you with a bunch of different errors so didn't show more about the same value.

1 Like

It's either working as intended or a shortcoming of the current compiler, depending on how you look at it.

There's a couple[1] concepts here. One, as you talk about, is the ability to supply an implementation which is more general than the trait method. "Loosening" the lifetimes is one example; just not mentioning some other bounds when you don't need them for your particular implementation is another example. The compiler has allowed this for... quite some time,[2] but doesn't allow consumers of the implementation to take advantage of the more general implementation.

That's what's going on in your playground. It's still acting like you used the signature in the trait.

There's another, newer concept called refinement, where you can opt into consumers of your implementation being able to take advantage of your more general implementation. Ideally,[3] you should have to explicitly opt-in to refinement, because if consumers can take advantage of it, it's a breaking change to replace your implementation with a less refined one (even if it's within the bounds of the trait).

So as an upside to your playground, you can change to the more restricted implementation without breaking downstream.

Rust recently got RPIT[4] in traits,[5] which does allow refinement: Playground.[6] I believe this is the only stable form of refinement available today.


RPIT (in or out of traits) also leaks auto traits. That is, if your opaque return type implements Send or Sync, etc, then consumers can take advantage of that, even if Send and Sync are not required by the RPIT. This is similar to refinement, but not really the same thing in some nuanced ways. It has the same invisible SemVer hazards as silent refinement: changing your implementation to something without the auto-traits is a breaking change. (These hazards have also existed since day 1 for structs.)


  1. or more ↩︎

  2. forever/1.0? I'm not going to check right now ↩︎

  3. IMO ↩︎

  4. fn(&self, ..) -> impl Trait ↩︎

  5. RPITIT ↩︎

  6. The lint doesn't even fire if your trait is private... ↩︎

1 Like

right code:

trait Foo<'a, 'b> {
    fn bar(&'a self) -> &'b i32;
}

impl<'a, 'b> Foo<'a, 'b> for () {
    fn bar(&'a self) -> &'b i32 {
    //or fn bar(&'a self) -> &'static i32 {
        &42
    }
}

fn main() {
    let a = {
        let b = ();
        let c = b.bar();
        // or let c: &'static i32 = b.bar();
        c
    };
    dbg!(a);
}

Thanks, I think you are right. I got confused by the fact that

let c: &'static i32 = ().bar();

works just fine, but I think that's because () gets promoted to a static variable in this case, while I thought of it as a temporary that only lives until the end of the statement.

Thanks for the details. The root of my confusion was my incorrect conclusion that the refinements are actually usable (see my previous reply).

I ran into RPIT leaking autotraits before and it's unfortunate, since it means you can make a breaking change just by changing a function body.

As a general suggestion, if you're exploring various semantics in Rust I suggest using String as the placeholder type. That way you don't accidentally trip the "but it's Copy" or "we promoted it for you" etc special cases.

2 Likes

I actually usually do that. I was just lazy in this case, and the combination of the somewhat suboptimal error message and let c: &'static i32 = ().bar() working fine led me to a wrong conclusion.