Interaction of `+ 'static` and `+ use<>` bounds

Is there any interaction between these two concepts? I would guess yes, since if something is 'static, it surely does not capture any (non-static) lifetimes.

This can be also illustrated with the following (which compiles and outputs ()):

use std::fmt::Debug;

fn foo<'a>(_: &'a String) -> impl Debug + 'static + use<'a> {}
// This is (as of edition 2024) just desugared version of
// fn foo(_: &String) -> impl Debug + 'static {}

fn main() {
    let s = "hello".to_owned();
    let x = foo(&s);
    drop(s);
    // `s` is dropped but is still borrowed by `x`.
    println!("{x:?}");
}

However, if I add indirection, it no longer compiles

use std::fmt::Debug;

fn foo<'a>(_: &'a String) -> impl Debug + 'static + use<'a> {}
// This is just desugared version of
// fn foo(_: &String) -> impl Debug + 'static {}

fn get_debug() -> impl Debug + 'static {
    let s = "hello".to_owned();
    foo(&s)
}

fn main() {
    let x = get_debug();
    println!("{x:?}");
}

with

   Compiling playground v0.0.1 (/playground)
error[E0597]: `s` does not live long enough
  --> src/main.rs:9:9
   |
8  |     let s = "hello".to_owned();
   |         - binding `s` declared here
9  |     foo(&s)
   |     ----^^-
   |     |   |
   |     |   borrowed value does not live long enough
   |     argument requires that `s` is borrowed for `'static`
10 | }
   | - `s` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` (bin "playground") due to 1 previous error

So, is this expected? I would expect either both of them to work or neither, because the first one seemingly leverages knowledge of that + 'static + use<'_> means that either '_: 'static or that the returned type actually does not capture the lifetime, so it must be the same thing as just + 'static.

The second example seems to not use reasoning like this, but then ends up with constraints that effectively enforce '_: 'static.

But more importantly, it's weird that the same function can be used with non-static lifetimes in one context and effectively cannot be in others.

Yes, there is an interaction. Let's look at a different scenario first:

fn example<T>(t: &T)
where
    T: ?Sized + 'static + Deref<Target: Display + Sized>
{
    let _: &(dyn Display + 'static) = &**t;
}

This compiles, even though we didn't have T::Target: 'static bound. Why? Because the compiler understands that if all the inputs to a trait implementation meet a 'static bound, the associated types meet a 'static bound too.

And something similar happens with -> impl Trait. If it meets a 'static bound, it can't materially hold on to a borrow (that's not 'static). So that's why your first example compiles -- the compiler recognizes that you couldn't have stored the &'a String in the return value.


But why doesn't your second one compile? It's because you can't completely forget about lifetimes even if they are not materially stored, as some type-system-relevant lifetime relationship may be lost if you were to do so. Perhaps you know about this famous soundness hole? As it turns out, something similar can happen if you just completely forget that the return type is use<'a>.

Here's the PR that allowed the first example to compile, which lays out some more details.

In effect there are multiple types of capturing: Capturing in the intuitive "materially held borrow" sense,[1] and capturing in a type-system relevant "ability to name a lifetime" sense.

Expected? For your typical programmer, probably not. Necessary? It at least some cases, yes. Are all sound cases accepted? Not currently and probably not ever, but more cases may be accepted over time (your first example fails before Rust 1.75, when the PR landed).


If you need the pattern of your second example, there may be an out: type erasure can erase lifetimes so long as implementing type can meet the resulting dyn lifetime. So this compiles:

fn get_debug() -> impl Debug + 'static {
    let s = "hello".to_owned();
    Box::new(foo(&s)) as Box<dyn Debug>
}

Even though foo's opaque return type captured the lifetime in some naming sense, it's still guaranteed to meet a 'static bound,[2] so it can be coerced to dyn Debug + 'static.


  1. these are what participate in : 'lifetime bounds ↩︎

  2. and a Debug bound and an implicit Sized bound ↩︎

Here's a related situation, where an associated type that's defined by way of a lifetime cannot be normalized and the lifetime is not allowed to be forgotten, even though the associated type meets a 'static bound.

trait Opaque<'a> {
    type Ty: Debug + 'static;
    fn make(self) -> Self::Ty;
}

fn get_debug<T>() -> impl Debug + 'static
where
    T: Default + 'static,
    for<'a> &'a T: Opaque<'a>,
{
    let s = T::default();
    // Ok (`'static` bound is still met)
    // Box::new(foo(&s)) as Box<dyn Debug>
    // Error
    foo(&s)
}

The return type of foo in get_debug is <T as Opaque<'local>>::Ty and cannot be normalized, and the lifetime that is part of this "rigid alias" cannot be soundly forgotten in the general case either.

(The analogous code may compile in a non-generic setting where T::Ty can normalize, and the normalized form no longer involves the lifetime. -> impl Trait doesn't normalize outside of the defining function.)

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.