Generic parameter lifetime?

playground

use std::rc::Rc;

enum TypedFunc<Arg> {
    Int(Rc<dyn Fn(Arg) -> i32>),
    Float(Rc<dyn Fn(Arg) -> f32>),
}

impl<Arg> TypedFunc<Arg> {
    fn exec_float(&self, arg: Arg) -> f32 {
        match self {
            Self::Int(f) => f(arg) as f32,
            Self::Float(f) => f(arg),
        }
    }
}

struct Foo {
    i: i32
}

fn main() {
    // Create a TypedFunc that expects a reference to Foo
    let getter = TypedFunc::Int(Rc::new(|obj: &Foo| obj.i));

    // Attempt to use TypedFunc with a temporary value
    let foo = Foo { i: 42 };
    dbg!(getter.exec_float(&foo));  // This line will cause an error
}

This code does not compile with error:

Error[E0597]: `foo` does not live long enough
  --> src/main.rs:28:28
   |
27 |     let foo = Foo { i: 42 };
   |         --- binding `foo` declared here
28 |     dbg!(getter.exec_float(&foo));  // This line will cause an error
   |                            ^^^^ borrowed value does not live long enough
29 | }
   | -
   | |
   | `foo` dropped here while still borrowed
   | borrow might be used here, when `getter` is dropped and runs the destructor for type `TypedFunc<&Foo>`

It looks like compiler decides that exec_float may store borrowed value of Foo, and therefore it is not allowed to drop it.

On the other hand, if I rewrite code without generic parameters, the compiler infers lifetimes and compiles it without problem:

enum TypedFunc {
    Int(Rc<dyn Fn(&Foo) -> i32>),
    Float(Rc<dyn Fn(&Foo) -> f32>),
}
impl TypedFunc {
    fn exec_float(&self, arg: &Foo) -> f32 {
        match self {
            Self::Int(f) => f(arg) as f32,
            Self::Float(f) => f(arg),
        }
    }
}

Is it possible to specify Arg lifetime, so that the compiler understands what's going on?

Correct. And it's easy to do so (make a closure that pushes every arg into a Vec).

It works because you changed the semantics. In the generic version, the problematic playground has Arg = &'one_specific_lifetime Foo, and the type-erased closure takes that Arg specifically. With the modification, the type-erased closure takes &Foo with any lifetime.[1] One generic equivalent is this:

enum TypedFunc<Arg> {
    Int(Rc<dyn Fn(&Arg) -> i32>),
    Float(Rc<dyn Fn(&Arg) -> f32>),
}

impl<Arg> TypedFunc<Arg> {
    fn exec_float(&self, arg: &Arg) -> f32 {
        match self {
            Self::Int(f) => f(arg) as f32,
            Self::Float(f) => f(arg),
        }
    }
}

Which also compiles.

Rust doesn't have generic type constructors...

// Made-up syntax that would accept both `Arg<'any> = i32` and
// `Arg<'specific> = &'specific Foo`
enum TypedFunc<Arg<'*>> {
    Int(Rc<dyn Fn(Arg<'_>) -> i32>),
    Float(Rc<dyn Fn(Arg<'_>) -> f32>),
}

...so if you don't want to have functions that have to take reference,[2] but can still take a &Foo with any lifetime, you need to jump through more hoops.[3]

Or just take the ergonomic hit in your original playground of having to declare foo before getter.


  1. Note that there's no safe way to store &Args with an arbitrarily short lifetime. ↩︎

  2. or another specific type constructor ↩︎

  3. And it's hard to keep it ergonomic and not destroy inference. ↩︎

1 Like

I would not say I really understand what's going on, but for now my theory is this:

With a function signature like exec_float(&self, arg: &Arg) the compiler will not allow storing arg inside the body of the exec_float function. And knowing this, it allows dropping the value after the call.

But for exec_float(&self, arg: Arg) it cannot guarantee this, as during the compilation of the function body it knows nothing about Arg lifetime, so it assumes that the value maybe stored.

I do not like how this sounds because the compiler actually does not know anything, it works by its rules. So there should be some specific terms in which one can think about it that match the inner workings of the compiler.

I believe the actual rule is "trait objects are assumed to have a drop implementation that examines their generic parameters". In this case that means examining &'local Foo, where 'local is tied to a borrow of foo.

Taking a reference[1] keeps the lifetime of the local borrow out of the trait parameters.


  1. or using something else higher-ranked ↩︎

I wasn't actually correct about that, since you have an implicit 'static on the type-erased closure.

Rc<dyn Fn(Arg) -> i32 /* + 'static */>

There would have to be some unsafe somewhere for an implementor to store non-'static Args and examine them later, while also meeting a 'static bound.

But borrow checking doesn't change semantics, lifetime based specialization is unsound, and dyn lifetimes are covariant, so I doubt it's possible to ever loosen the "dyn Trait<Arg> examines Arg on drop" check based on that implicit 'static lifetime.

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.