Lifetimes: Why this code doesn't compile?

I want to create a trait that will contain a set of functions. But when I compile this code:

trait FnSet<T> {
    const FUNCTIONS: &'static [fn(&T)];
}

The compiler prints an error:

error[E0310]: the parameter type `T` may not live long enough
 --> src/main.rs:3:5
  |
2 | trait FnSet<T> {
  |             - help: consider adding an explicit lifetime bound...: `T: 'static`
3 |     const FUNCTIONS: &'static [fn(&T)];
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the reference type `&'static [for<'r> fn(&'r T)]` does not outlive the data it points at

Usually, everything that can be defined as a constant I make a constant. But I see no reason why I couldn't create a constant array of function pointers.
As I understand, the lifetime annotation allows compiler to check a data in memory is alive. But why is this necessary when I have no data? I just have function pointers that own nothing.

I could add T: 'static bound, but I want this trait to be able to be implemented for structs with references.

2 Likes

Yes, here you are hitting a limitation of the type checker / borrow checker that, imho is unnecessary: it's not because a function's parameter is not 'static that the pointer itself should be prevented from being 'static. And yet that's how Rust currently works :slightly_frowning_face:. I think this happens because the moment some type parameter is mentioned in a type, including a function pointer type, then the whole type is considered to be bounded by the lifetime of that type parameter among other things).


To circumvent this, you have to add an explicit generic lifetime parameter:

- trait FnSet<T> {
+ trait FnSet<'t, T : 't> {
-     const FUNCTIONS: &'static [fn(&T)];
+     const FUNCTIONS: &'t [fn(&T)];
  }
  • Note that if you take 't = 'static, this would match the "use T : 'static" solution.

And to impl it, if you have a Struct with a generic <'lifetime> parameter, you write:

impl<'lifetime> FnSet<'lifetime, Struct<'lifetime>> for … {
    const FUNCTIONS: &'lifetime [fn(&Struct<'lifetime>)] = &[
        // some_fun,
    ];
}

or

impl<'a, 'lifetime : 'a> FnSet<'a, Struct<'lifetime>> {
    const FUNCTIONS: &'a [fn(&Struct<'a>)] = &[
        // some_fun,
    ];
}

For the non-generic structs / types, just imagine that 'lifetime is 'static:

impl FnSet<'static, Struct> for … { … }
// or:
impl<'a> FnSet<'a, Struct> for … { … }
3 Likes

Another alternative is to use

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
}

which does compile. IMO this demonstrates that there really is no good reason why the const FUNCTIONS: ... version didn’t work. If there isn’t already something about this, it could IMO be worth it to open an issue about this.

3 Likes

Thanks for answers!
It's great that this is only a temporary flaw of the compiler, and not a fundamental feature of the language.

Indeed it is quite surprising that &'static [fn(&T)] is both:

  • a well-defined type,

  • not a : 'static type (because fn(&T) is not 'static).

So the assumption that given two types T and U so that type T = &'lifetime U; is a usable type, we don't necessarily have U : 'lifetime and thus don't have T : 'lifetime either.

  • EDIT: by that I mean that Rust's constraint solver does not consider that these types satisfy thouse bounds, evn though they must do so in order for the type T to be usable and thus well-formed

Very surprising indeed.

Wait a minute, maybe I'm not understanding something correctly, but how is this not unsafe? I thought &'lifetime T always implied T: 'lifetime otherwise you can get an invalid reference pointing into freed memory, no?

It's possible for T: 'lifetime to be true and implied without the compiler being able to explicitly realize it. In such a case there's nothing unsafe, only that some safe programs that couldn't be written. Unless you can actually construct a T that violates T: 'lifetime, then there's a real danger.

Not saying that's how it is, but it's a possibility.

I’ve opened an issue:

1 Like

Got some quirky behavior out of this (and similar things). Apparently, stating a return type of a function like this kind-of defers the lifetime-check to the caller...

e.g. this compiles fine, too:

fn foo<'a>() -> &'static &'a () {
    &&()
}

but using it as in

fn bar<'a>() {
    foo::<'a>();
}

doesn’t work.

Going back to the example at hand, here are a few things that I tried:

This doesn’t compile

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
    fn use_functions() {
        Self::functions();
    }
}

This does compile:

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
    fn use_functions();
}

impl<'a> FnSet<&'a ()> for() {
    fn functions() -> &'static[fn(&&'a ())] {
        &[]
    }
    fn use_functions() {
        Self::functions();
    }
}

fn bar<'a>() {
    <() as FnSet<&'a ()>>::use_functions();
}

This doesn’t:

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
}

impl<'a> FnSet<&'a ()> for() {
    fn functions() -> &'static[fn(&&'a ())] {
        &[]
    }
}

fn bar<'a>() {
    <() as FnSet<&'a ()>>::functions();
}

But isn't that what @Yandros claimed in his post above? He said it was surprising that &'static [fn(&T)] is a well-defined type because fn(&T) is not 'static and so [fn(&T)] is not 'static and so if we simplify by substituting in some type variables we essentially have a &'lifetime T where T: !'lifetime and yet the compiler is still compiling it without raising any errors?

Actually, is it true that fn(&T) is not 'static? This seems to work just fine:

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
}

fn takes_num_ref(_x: &i32) {}
fn also_takes_num_ref(_x: &i32) {}

struct Struct;

impl FnSet<i32> for Struct {
    fn functions() -> &'static [fn(&i32)] {
        &[
            takes_num_ref,
            also_takes_num_ref,
        ]
    }
}

fn main() {
    let funcs = Struct::functions();
    for num_ref in &[1, 2, 3] {
        for func in funcs {
            func(num_ref);
        } 
    }
}

playground

Or is the issue that fn(&T) can potentially be non-'static? If so can I see an example because I can't think of any way off the top of my head on how to make a non-'static function pointer...

In your example you have T == i32 and i32: 'static. Problems may arise with e.g. T == MyStructWithReferences<'lifetime>.

I think everyone is agreeing here that at least when T: 'static, then fn(&T): 'static.

I don't think there's a safe way to do it, but I believe you can have function pointers into dynamically loaded libraries, and those could have non-static lifetime constraints. But that's probably not relevant here because you won't be able to put them into a static array.

I think everyone is agreeing here that at least when T: 'static , then fn(&T): 'static .

This is confusing to me, because I can't imagine why lifetime of the function's arguments have any bearing on whether the function itself exists. You can obviously create a function before you pass arguments into it, so you can create a reference to the function that has a longer lifetime than the lifetime of its arguments. Unless I'm misunderstanding something...

What exactly do you mean by confusing. Do you disagree? I was saying "at least" to say that that statement is uncontroversion, but I’m not implying that any fn is not 'static. Just saying that for sure fn(&T) is 'static if T: 'static. If T: !'static, I don’t know. And AFAICT the compiler currently seems to be of the opinion that fn(&T) isn’t static in this case.

I am equally confused, and I believe for the same reason as @skysch: I don't understand why the lifetime of a function pointer's argument would have any bearing on the lifetime of the function pointer itself... and yet... this does not compile:

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
}

fn takes_nonstatic(_x: &NonStatic<'_>) {}
fn also_takes_nonstatic(_x: &NonStatic<'_>) {}

struct NonStatic<'a>(&'a i32);

struct Struct;

impl FnSet<NonStatic<'_>> for Struct {
    fn functions() -> &'static [fn(&NonStatic<'_>)] {
        &[
            takes_nonstatic,
            also_takes_nonstatic,
        ]
    }
}

fn main() {
    let nums = [0, 1];
    let nonstatics = [
        NonStatic(&nums[0]),
        NonStatic(&nums[1]),
    ];
    let funcs = Struct::functions();
    for nonstatic in &nonstatics {
        for func in funcs {
            func(nonstatic);
        } 
    }
}

playground

Throws:

error[E0597]: `nums[_]` does not live long enough
  --> src/main.rs:24:19
   |
24 |         NonStatic(&nums[0]),
   |                   ^^^^^^^^ borrowed value does not live long enough
...
28 |     for nonstatic in &nonstatics {
   |                      ----------- argument requires that `nums[_]` is borrowed for `'static`
...
33 | }
   | - `nums[_]` dropped here while still borrowed

error[E0597]: `nums[_]` does not live long enough
  --> src/main.rs:25:19
   |
25 |         NonStatic(&nums[1]),
   |                   ^^^^^^^^ borrowed value does not live long enough
...
28 |     for nonstatic in &nonstatics {
   |                      ----------- argument requires that `nums[_]` is borrowed for `'static`
...
33 | }
   | - `nums[_]` dropped here while still borrowed

Why does a function pointer which can take a non-'static argument also become non-'static?

What I am saying is rather on the side of false positives, i.e., of Rust being overly conservative: if type T = &'lifetime U is a usable type (e.g., can be used as the return value of a function), then it is not unsound for T (and U) to verify that each is : 'lifetime. And yet they don't happen to be viewed as such per Rust "constraint solver": that's what I meant by "they are not : 'static".

I’m having the suspicion that the above code actually gets interpreted as

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
    fn use_functions();
}

impl<'a> FnSet<&'a ()> for() {
    fn functions() -> &'static[fn(&&'a ())] {
        &[]
    }
    fn use_functions() {
        <Self as FnSet<&'static ()>>::functions();
    }
}

fn bar<'a>() {
    <() as FnSet<&'a ()>>::use_functions();
}

rather than

trait FnSet<T> {
    fn functions() -> &'static [fn(&T)];
    fn use_functions();
}

impl<'a> FnSet<&'a ()> for() {
    fn functions() -> &'static[fn(&&'a ())] {
        &[]
    }
    fn use_functions() {
        <Self as FnSet<&'a ()>>::functions();
    }
}

fn bar<'a>() {
    <() as FnSet<&'a ()>>::use_functions();
}

(and the latter does not actually compile)

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.