Refactoring variables on the stack to variables in a struct: how to deal with lifetime parameters?

I'm using a crate that defines a struct that accepts a lifetime parameter for a Cow reference it stores. Idiomatically these structs are intended to be used on the stack. See Block in ratatui::widgets::block - Rust -> Title in ratatui::widgets::block::title - Rust -> Line in ratatui::text - Rust -> Span in ratatui::text - Rust. However, a second crate embeds this struct and exposes that struct publicly. See textarea.rs - source

And then, if I embed this struct in my own hierarchy, the lifetime requirements seem to be required all the way up for every containing struct. For a single string nested fairly deeply this seems sub-optimal.

The code below exemplifies the problem. The containing function is an example of working code that requires no explicit lifetime when constructing a Nested<'a>. I then attempt to refactor this function into a struct (something I often want to do when functions get too long...), and I'm left with either the compiler error shown, or a need to propagate the explicit lifetime all the way up, for every containing struct.

Is there a way I can "fix" this without changing the Nested type at all (since I have no control over it).

fn containing() {
    let a: String = "a".into();
    let _n = Nested { b: &a };
}

struct Containing {
    a: String,
    nested: Nested,
}

struct Nested<'b_lifetime> {
    b: &'b_lifetime str,
}

impl Containing {
    fn new(a: String) -> Containing {
        Containing {
            a,
            nested: Nested { b: &a },
        }
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
 --> src/lib.rs:8:13
  |
8 |     nested: Nested,
  |             ^^^^^^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
6 ~ struct Containing<'a> {
7 |     a: String,
8 ~     nested: Nested<'a>,
  |

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

you are creating a self-referential structure. this is an indicator that either you are using the API wrong, or the API design is flawed.

I don't have the time to look at the code, but:

it seems to me you are mis-understanding the concept of lifetime. if you are returning a type with lifetime parameters, the lifetime must come from somewhere, either from the function arguments, or you can only return values with 'static.

that's just the way lifetime works, unless you put a 'static somewhere in the wrapping hierarchy.

lifetimes are generic parameters to the types, and you cannot create values with arbitrary lifetimes out of thin air (except you "leak" a heap allocated object, or through an unsafe cast/transmute), the only lifetime that doesn't need to be "derived" from another lifetime is 'static.

2 Likes

If you want to avoid that, use 'static for the lifetime and only use literals ("...", which are &'static strs) or owned variants. You'll lose the ability to hold a borrow of a run-time value (borrowed String) -- but if all you're doing with the String is using it for the Nested<'_>, this might not even be a concern.

If you have

struct Container {
    // just keep the owned `String` in here
    nested: Nested<'static>,
}

Then &some_container.nested can almost surely still be coerced to a non-'static &Nested<'_> (but I didn't test this against the crates in question). This may or may not be relevant to / enough for your use case.

The lifetime is implicit and inferred by the compiler, not absent. In the example, it's only as long as the single line, since it's not used anywhere. If you used it, the lifetime would be valid wherever the uses require it.

You can't return it from containing because it references a local variable (it would dangle when a drops). If you move the borrowed String, the borrowing Nested can no longer be used. That includes trying to return both the String and the Nested (which would be some form of self-referencial struct).


You can technically create a self-referential Containing<'_>, but it's pretty much always too limited to be useful, because once everything is wired up (once it becomes self-referential), it is exclusively borrowed forever -- you can't use it directly ever again, you can't move it, and depending on the fields, you may not even be able to construct it.

In other words, you might as well just used Nested<'_> with a String in a separate stack variable -- i.e. back where you started with fn containing -- because it's less restricted than the self-referencial struct.

1 Like

Thanks @nerditation and @quinedot , that is pretty much what I've understood to be the case about the situation, at least for straightforward and idiomatic Rust (though I'm too new to Rust to express it with any precision).

In direct answer to my search for an "escape hatch" for this problem I found two crates that address it directly: https://crates.io/crates/ouroboros and https://crates.io/crates/self_cell. They both encapsulate some unsafe rust behind an API and give you a struct with some methods.

In answer to my actual problem, it turns out that ratatui embeds a Cow<'a, str> in a struct, which fairly easily accepts a String when the lifetime is `'static':

struct Container {
    cow: std::borrow::Cow<'static, str>,
}

fn example(string: String) -> Container {
    Container {
        cow: string.into(),
    }
}

The thing that could be improved here, I think, is the ratatui documentation, which doesn't make clear what this lifetime is for or what the implications of 'static is for the type.

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.