Why does the compiler think this value doesn't live long enough?

I am truly confused by this minimal example. For the life(time) of me, I cannot figure out why this value does not live long enough.

The actual concrete return type of format_with_config() should be String in all of these minimal examples, however I'm trying to get it to work with impl Debug.

I even tried to be as explicit as possible that the return value is not tied to the lifetime of &self by declaring impl Debug + 'static.

The example works if you replace all of the impl Debug + 'static with String, so I know that the intent here is valid and logically sound, but I really would like to try to get this working with impl <trait>.

The function returns an owned value, so I don't understand why the compiler thinks the borrow is invalid. What else do I have to do here to make this work?

Thank you for any insights you might be able to share!

use core::fmt::Debug;

#[derive(Debug, Default)]
struct Config {}

#[derive(Debug)]
struct Configurable<'config, T> {
    item: T,
    config: &'config Config,
}

impl<'config, T> Configurable<'config, T> {
    fn share_config<R>(&self, other: R) -> Configurable<'config, R> {
        Configurable {
            item: other,
            config: self.config,
        }
    }
}

trait FormatWithConfig {
    fn format_with_config<T: Debug>(&self, other: T) -> impl Debug + 'static;
}

#[derive(Debug, Clone)]
struct Value {}

impl<'config> FormatWithConfig for Configurable<'config, &Value> {
    fn format_with_config<T: Debug>(&self, other: T) -> impl Debug + 'static {
        format!("{:?}", other)
    }
}

struct ValueWithCount<'value> {
    value: &'value Value,
    count: usize,
}

impl<'value, 'config> FormatWithConfig for Configurable<'config, ValueWithCount<'value>> {
    fn format_with_config<T: Debug>(&self, other: T) -> impl Debug + 'static {
        let configured_value = self.share_config(self.item.value);
        configured_value.format_with_config(other)
    }
}

fn main() {}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `configured_value` does not live long enough
  --> src/main.rs:42:9
   |
40 |     fn format_with_config<T: Debug>(&self, other: T) -> impl Debug + 'static {
   |                                     - let's call the lifetime of this reference `'1`
41 |         let configured_value = self.share_config(self.item.value);
   |             ---------------- binding `configured_value` declared here
42 |         configured_value.format_with_config(other)
   |         ^^^^^^^^^^^^^^^^--------------------------
   |         |
   |         borrowed value does not live long enough
   |         argument requires that `configured_value` is borrowed for `'1`
43 |     }
   |     - `configured_value` dropped here while still borrowed

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

It compiles if you inline the variable configured_value:

impl<'config, 'value> FormatWithConfig for Configurable<'config, ValueWithCount<'value>> {
    fn format_with_config<T: Debug>(&self, other: T) -> impl Debug + 'static {
        self.share_config(self.item.value).format_with_config(other);
    }
}

That is definitely a weird situation and I hope that someone else can explain it. It smells like one of those strange corner cases of the borrow checker that are very underdocumented.

Thanks for the reply here!

Though I think you may have returned the unit type by adding a semicolon to your one-liner, which is compatible with impl Debug + 'static.

I suspect you'll get the same error if you remove the semicolon.

Yes it is exactly that. Very subtle. I have just reproduced the same.

Sorry, that was a mistake on my part, and doesn't help you. Let me get back to you.

Seems like a bug related to trait methods returning impl trait to me. If you remove the FormatWithConfig trait from the equation (or just from the Configurable impl), it compiles just fine. I found return_position_impl_trait_in_trait can not express static lifetime bound · Issue #117210 · rust-lang/rust · GitHub where a reply says that it happens due to impl-trait return type is bounded by all input type parameters, even when unnecessary · Issue #42940 · rust-lang/rust · GitHub

2 Likes

Ah, this does look relevant to the issue! Thank you for finding this.

I wonder if using a trait is just off the table because of this, or if there's a way to make it work.

This is amongst known limitations of -> impl Trait + 'a return types as far as I know. I’m not sure what exactly the latest situation was for non-trait ones – it doesn’t help that the rules what parameters (lifetimes and type parameters) become part of the opaque return type also differs between the two situations (which is also a deliberate design decision, in order to make async fn in traits be equivalent to -> impl Future returning functions).

Anyways, the situation as far as I’m aware is that a type like dyn Trait + 'static can be a bit schizophrenic in that it requires for the actual type you provide in the function body to be implementing 'static, but the overall type becomes something of an Opaque<Params…> type which captures type parameters (and possibly even lifetime parameters) from the function body – in case of impl Trait return types in trait methods, that’s all lifetime parameters, in case of outside of traits only the ones you mention in the type.

So fn format_with_config<T: Debug>(&self, other: T) -> impl Debug + 'static signatures come with some type definition Opaque<'a, T> and then have the signature fn format_with_config<'a, T: Debug>(&'a self, other: T) -> Opaque<'a, T>, with the effect that if 'a is not 'static or T is not …: 'static, then the opaque types isn’t …: 'static either.

I do remember having seen some work on improving this situation, I’m not sure off the top of my head what improvements to the original situation are already in place.


Edit: Looks like Consider alias bounds when computing liveness in NLL (but this time sound hopefully) by compiler-errors · Pull Request #116733 · rust-lang/rust · GitHub is the degree to which the issue has been addressed. (Now we just gotta understand what cases this PR actually does and doesn’t fix.)

2 Likes

These have all been very helpful explanations and insights, everyone.

Thank you for all of the support.

It's overcapturing in RPIT: 3498-lifetime-capture-rules-2024 - The Rust RFC Book

One solution as given from the link is via TAIT: Rust Playground

2 Likes

Jon Gjengset gave recently a lecture at the University of Copenhagen where he also talks about the current state of impl Trait - APIT ATPIT, RPIT, RPITIT... and so on. The current and the future capturing behaviour is also mentioned. I can highly recommend watching the recording: https://www.youtube.com/watch?v=CWiz_RtA1Hw

1 Like