What does a static lifetime of a Fn closure type mean?
Asked Answered
I

4

8

The following error goes away if I do as rustc tells me to, and change the bound to

where F: Fn() -> () + 'static

pub struct Struct(Box<dyn Fn() -> ()>);
pub fn example<F>(f: F)
where
    F: Fn() -> ()
{
    Struct(Box::new(|| ())); // ok
    Struct(Box::new(f)); // error: The parameter type `F` may not live long eneough
                         // help: consider adding an explicit lifetime bound: `F: 'static`
}

However, I just don't understand what 'static means here. It doesn't seem to mean that the closure itself lives forever.

At least, it doesn't seem like the closure lives forever. If I have the closure own some data, that data gets dropped at some point, which makes me suspect the closure is dropped:

pub struct Struct(Box<dyn Fn() -> ()>);

#[derive(Debug)]
struct DropMe;
impl Drop for DropMe {
    fn drop(&mut self) {
        println!("dropped");
    }
}

/// prints:
///     "got DropMe"
///     "got DropMe"
///     "dropped"
///     "end of program"
pub fn main() {
    let d = DropMe;
    example(move || {
        println!("got {:?}", d);
    });
    println!("end of program")
}

pub fn example<F>(f: F)
where
    F: Fn() -> () + 'static
{
    let s = Struct(Box::new(f));
    s.0();
    s.0();
}

Is 'static an upper bound on the lifetime of the closure rather than a lower bound? 'static makes sense as an upper bound, then example would be saying "give me a function that I can hold on to as long as I want", which in practice can be less than the 'static lifetime.

How can I tell when + 'lifetime adds 'lifetime as an upper bound vs. a lower bound? The Rustonomicon chapter on subtyping+variance doesn't seem to cover this, and I couldn't find the information I was looking for in the Rust Book or Rust Reference.

Inclined answered 26/12, 2021 at 19:54 Comment(0)
W
6

T: 'a isn't a constraint on the lifetime of instances of T, but of things that T borrows (if any): that is, all borrows in T must outlive 'a.

Thus F: Trait + 'static requires that any borrows in F be for 'static lifetime (or longer, which doesn't exist), regardless that Trait in this case is Fn() -> ().

In your case, the closure takes ownership of d (and borrows the &'static str literal); hence it satisfies F: 'static. But if instead of move || ... the closure merely borrowed d with || ..., then it would not be able to satisfy 'static (as the lifetime of the borrow of d cannot exceed the scope of the call to main).

Whistler answered 26/12, 2021 at 20:37 Comment(0)
B
4

Yes, 'static is an upper bound.

In fact, all lifetimes constraints are upper bounds. For the callee (that is, whom uses the lifetime).

For the caller (i.e. the provider of the lifetime), on the other hand, they're usually lower bounds: give me something that lives at least as 'static (of course, nothing lives more than 'static, so it actually means "give me something 'static". But it does matter when talking about other lifetimes).

Variance is about changing the caller's respect regarding the lifetime: whether it can pass a longer lifetime (covariance, i.e. a lower bound), a shorter lifetime (contravariance, i.e. an upper bound), or only exactly this lifetime (invariance).

Brana answered 26/12, 2021 at 22:13 Comment(2)
Thanks for this answer. I'm not sure the "for the caller" part is correct. In my second example, caller is main and callee is example. example asks for an arg with a 'static lifetime. You wrote that, from the caller's POV, the lifetime is a lower bound. If that's the case, then the second example shouldn't compile, but it does. f's lifetime ends before the static lifetime: the closure is droped before "end of program" is printed. What I think is happening is that 'static is an upper bound. The compiler picks a lifetime that ends just before println!("end of program").Inclined
From main()'s POV, the closure is 'static: because you move the DropMe into it, it controls it, meaning it'll drop when the closure will. The closure does not hold any reference to the outside. example() can drop it before, like I said: for the callee, this is a an upper bound.Brana
C
3

The 'static variant is not obligatory. It seems like it's just what compiler proposes by default. The two edits below should hopefully illustrate the capture of lifetime of variables from environment.

//rustc 1.62.1 (e092d0b6b)
pub struct Struct<'a>(Box<dyn Fn() -> () + 'a>);

#[derive(Debug)]
struct DropMe;
impl Drop for DropMe {
    fn drop(&mut self) {
        println!("dropped");
    }
}

pub fn main() {
    let d = DropMe;
    let x = 43;

    example(move || {
        println!("got {:?} {:?}", d, &x);
    });
    println!("end of program")
}

pub fn example<'env_borrow, F>(f: F)
where
    F: Fn() -> () + 'env_borrow
{
    let s = Struct(Box::new(f));
    s.0();
    s.0();
    // drop(s);
    println!("end of higher bound func");
}

// rustc 1.62.1 (e092d0b6b)
pub struct Struct<'a>(Box<dyn FnMut() -> () + 'a>);

#[derive(Debug)]
struct DropMe;
impl Drop for DropMe {
    fn drop(&mut self) {
        println!("dropped");
    }
}

pub fn main() {
    let d = DropMe;
    let mut x = 43;
    let y = &mut x;

    let closure_boxed = example(move || {
        println!("got {:?} , *&mut x = {:?}", d, y);
        *y += 1;
    });

    // drop(x);
    // ^ ERROR: try uncomment
    drop(closure_boxed);
    // ^ ERROR: try comment
    println!("x is now {}", x);
    println!("end of program")
}

pub fn example<'env_borrow, F>(f: F) -> Struct<'env_borrow>
where
    F: FnMut() -> () + 'env_borrow
{
    let mut s = Struct(Box::new(f));
    s.0();
    s.0();
    s
}
Cooks answered 20/8, 2022 at 13:6 Comment(0)
C
1

I believe the issue is that Struct has no generic lifetime parameters, which means the lifetime of one of its instances has no name. The function an instance of Struct boxes (obviously) has to live as long as the instance, but the only way to guarantee that to the compiler is to give it a named lifetime. But because Struct has no lifetime parameters, the only named lifetime that is guaranteed to last sufficient long is 'static.

To fix this, you can give Struct a generic lifetime parameter and then constrain the boxed function to also only live that long.

pub struct Struct<'a>(Box<dyn 'a + Fn() -> ()>);
pub fn example<F>(f: F)
where
    F: Fn() -> (),
{
    Struct(Box::new(|| ())); // ok
    Struct(Box::new(f)); // also ok; 'a will be inferred from the lifetime of f
}
Catherine answered 26/12, 2021 at 20:30 Comment(1)
Thanks, but I think you may have misunderstood my question. There's no issue to solve with different code: as I explained, I did what rustc told me to do and the compiler error went away. What I was looking for was a better understanding of what 'static means here.Inclined

© 2022 - 2024 — McMap. All rights reserved.