Designating longer lifetime of function parameter returned via closure

Hello everyone,

I've been working through chapter 13.3 of the book about improving minigrep with closures and iterators. While playing around I've found an issue that the compiler correctly rejected to compile, but I'm a bit confused how the compiler knew and how this could be somehow annotated in the function declaration.

fn foo(data: &str) -> impl FnOnce() -> () {
    move || { println!("{data}") }
}

fn main() {
    let c = foo(&String::from("test"));
    c();
}

This doesn't compile, since the borrowed reference to the String created from "test" is lost after calling foo. And if you pass in "test" directly as a literal it does compile, as it has a static lifetime. So far so good. But just from the foo function declaration it has zero information that the passed parameter needs to have a longer lifetime. So my guess is the compiler does deeper analysis in those cases than just to look at the function declaration? It actually analyses the function definition/implementation as well?

The second question I have in this case: How do you mark the parameter data in foo to have a longer lifetime? If you'd provide such a public interface, it would probably be quite confusing for a caller to to be confronted with this situation. Thus it would be nice to designate the longer lifetime of data somehow in the function declaration. Is there a possibility for that?

Thank you for the help in advance.

It’s implicit when you write an -> impl ... return type[1]. The default[2] is to consider the opaque return type to “capture” all lifetimes in the function’s parameters — and the type &str has a lifetime whether or not you write one explicitly. Thus, what you have written is equivalent to the explicit form

fn foo<'a>(data: &'a str) -> impl FnOnce() -> () + use<'b> {

use<'lt> is the (newish) syntax for expressing that this capturing is happening, and controlling it. If you changed the signature to + use<> to capture nothing, you’d get a different error telling you that foo is invalid rather than that main is invalid:

fn foo(data: &str) -> impl FnOnce() -> () + use<> {
    move || { println!("{data}") }
}
error[E0700]: hidden type for `impl FnOnce()` captures lifetime that does not appear in bounds
 --> src/main.rs:2:5
  |
1 | fn foo(data: &str) -> impl FnOnce() -> () + use<> {
  |              ----     --------------------------- opaque type defined here
  |              |
  |              hidden type `{closure@src/main.rs:2:5: 2:12}` captures the anonymous lifetime defined here
2 |     move || { println!("{data}") }
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

No, it does not. There is only one exception where information from the function body is used — opaque -> impl types implement Send and Sync according to whether the concrete type in the function body does. Everything else, including lifetimes, is determined from the signature alone.


  1. sometimes known as Return Position Impl Trait (RPIT) ↩︎

  2. as of Rust Edition 2024 ↩︎

1 Like

you don't need a 'static lifetime here, you just can't use a reference to a temporary value. i.e. this also compiles:

    let s = String::from("test");
    let c = foo(&s);
    c();

this is a case where temporary lifetime extension does NOT apply. see: Destructors - The Rust Reference, specifically, there's an "Examples" section and your code is exactly the first example of the non-working ones.

for completeness, the type signature of foo() is essentially:

fn foo<'a>(data: &'a str) -> SomeOpaqueTypeWithLifetime<'a>;

it just happens you have an impl trait here, but any type that captures the input lifetime is sufficient to trigger this error, it doesn't matter whether it's named type or opaque type. for example, the above mentioned example in the documentation uses core::convert::identity(), which is generic and has a type signature like this: fn<T>(T) -> T.

1 Like

Thank you a lot. That helped clarify things for me. Same compiler error with:

fn foo(data: &str) -> impl FnOnce() -> () {
    if data.is_empty() {
        || { println!("empty") }
    } else {
        || { println!("non-empty") }
    }
}

fn main() {
    let c = foo(&String::from("test"));
    c();
}

The lifetime of data is, as you said, automatically captured via the return type. Even though it isn't used in the closure. But then writing it as follows works:

fn foo(data: &str) -> impl FnOnce() -> () + use<> {
    if data.is_empty() {
        || { println!("empty") }
    } else {
        || { println!("non-empty") }
    }
}

fn main() {
    let c = foo(&String::from("test"));
    c();
}

As I tell the compiler the lifetime doesn't need to be captured and the compiler agrees with me while compiling the function.

I guess the book hasn't caught up to such topics yet? I might need to read up on the RFCs and edition information at some point.