When can 'static types have store non static references?

I'm trying to understand why the following code does not compile:

trait X: 'static {
    fn map(&self) -> impl X;
}

fn map_x(x: &impl X) -> impl X {
    x.map()
}

With the error being:

error[E0700]: hidden type for `impl X` captures lifetime that does not appear in bounds
  --> src/main.rs:10:5
   |
9  | fn map_x(x: &impl X) -> impl X {
   |             -------     ------ opaque type defined here
   |             |
   |             hidden type `impl X` captures the anonymous lifetime defined here
10 |     x.map()
   |     ^^^^^^^
   |
help: to declare that `impl X` captures `'_`, you can add an explicit `'_` lifetime bound
   |
9  | fn map_x(x: &impl X) -> impl X + '_ {

From my understanding, since X derives from 'static, wouldn't it be impossible for any implementation of the map function in X to return a new X that borrows from the original one, and hence the + '_ would be extraneous?

This is an accurate statement, but the compiler isn't using this information. It potentially could, but RPIT in traits always captures all input lifetimes, i.e. the trait you've written is equivalent to

trait X: 'static {
    fn map<'a>(&'a self) -> impl X + 'a;
}

and fn map_x sees this capture and that's what causes the error. If you write out the associated type form without the implicit lifetime capture, e.g.

trait X: 'static {
    type Output: X;
    fn map(&self) -> Self::Output;
}

fn map_x(x: &impl X) -> impl X {
    x.map()
}

then you won't see any error.

Yes, this means lifetime capture for RPIT is different between trait and non-trait fn. The lang team chose to make RPIT in trait always capture because they believe this is more useful behavior for the sugar, the behavior of -> impl Trait + 'a and -> impl Trait + Captures<'a> is subtly different, and the lifetime capture behavior for non-trait RPIT is changing to match in-trait RPIT in the 2024 edition (meaning the OP example will compile as-is).

4 Likes

To explain one consequence of @CAD97's explanations: You can make map_x work by explicitly capturing the lifetime in question. The approach that works most generally would be to use a trait like

trait Captures<'a> {}
impl<T: ?Sized> Captures<'_> for T {}

If you then do

fn map_x<'a>(x: &'a impl X) -> impl X + Captures<'a> {
    x.map()
}

or equivalently (using elided lifetimes)

fn map_x(x: &impl X) -> impl X + Captures<'_> {
    x.map()
}

then the code compiles.

In this particular case, with only a single lifetime to be captured and a + 'static supertrait bound already in place, it turns out using + '_ is pretty much equivalent, too (which would look like this); but using Captures<'a> (possible multiple times, Captures<'x> + Captures<'y> + ...) is the most general approach.


Unfortunately; whereas as demonstrated above you can rewrite the freestanding function's signature to match the trait method and make the code work; there is no way (yet) to go the other way, and rewrite the signature inside of the trait in a way that the lifetime isn't captured (whilst still allowing you not to have to name the concrete return type). Even though if this was possible, this would be the preferred solution in this case, where due to the + 'static lifetime constraints, really capturing the lifetime is entirely pointless (and not really possible), anyways.

3 Likes

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.