Why does Rust care about lifetime of generic parameter?

I'm puzzled why the code below doesn't compile. I specifically used PhantomData<fn(T) -> ()> instead of PhantomData<T> because I thought doing so would remove T from being considered to affect how long the borrow checker thinks instances of Y<T> must live b/c it doesn't create a variance relationship. What is the right way to indicate, "this generic argument exists purely for type hackery and I am really really really never going to instantiate it so please stop caring about what it might hold" ?

use std::marker::PhantomData;

trait X<T: 'static>: 'static {}

struct Y<T>(PhantomData<fn(T) -> ()>);

impl<T> X<u8> for Y<T> {}

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0310]: the parameter type `T` may not live long enough
 --> src/lib.rs:7:9
  |
7 | impl<T> X<u8> for Y<T> {}
  |      -  ^^^^^ ...so that the type `Y<T>` will meet its required lifetime bounds
  |      |
  |      help: consider adding an explicit lifetime bound...: `T: 'static`

error[E0310]: the parameter type `T` may not live long enough
 --> src/lib.rs:7:9
  |
7 | impl<T> X<u8> for Y<T> {}
  |      -  ^^^^^ ...so that the type `T` will meet its required lifetime bounds
  |      |
  |      help: consider adding an explicit lifetime bound...: `T: 'static`

Playground link

1 Like

Not possible in Rust today.

I don't think this is actually related to variance per se; the compiler just doesn't care at all how a lifetime is used when it comes to well-formedness checks, which might be considered a bug with regard to fn. I think there might be an issue for it already, but I can't find it so I may be wrong.

This fails to compile because the generic signature of Y does not provide the implicit 'static lifetime that X does. If you add a constraint so that impl<T> becomes impl<T: 'static>, the code will compile:

use std::marker::PhantomData;

trait X<T: 'static>: 'static {}

struct Y<T>(PhantomData<fn(T) -> ()>);

impl<T: 'static> X<u8> for Y<T> {}

When you write impl<T> X<u8> for Y<T> {} that means you are trying to satisfy the X trait's requirements for every possible type T. However the compiler sees that this isn't possible and is telling you to constrain T more.

To provide an example for why you can't satisfy the X trait with impl<T> X<u8> for Y<T>, consider what we'd see if T was a &'short str. You would be trying to do impl<'short> X<&'short str> for Y<&'short str>, but &'short str doesn't satisfy the 'static bound on X's type parameter.

Edit: Ignore me, I thought the original post was impl<T> X<T> for Y<T> (i.e. implementing X<T> instead of X<u8>). Serves me right for not properly reading the code :man_facepalming:

thanks for pointing this out @Cerber-Ursi.

The Rust compiler doesn't care if you will never instantiate a dodgy impl. Unlike C++ templates where generic code is only type checked when it is instantiated, Rust generics are type checked when the generic code is defined.

That means it is the library author's job to add sufficient constraints to your T so that the impl is valid for any possible type satisfying those constraints.

1 Like

But they wouldn't. They would be trying to do impl<'short> X<u8> for Y<&'short str>, and X's type parameter here is obviously 'static. Problem is, Y<&'short str> is not 'static, however, by definition X<T>: 'static.

I think my general confusion here is I don't understand the philosophy of why sometimes Rust is willing to look at fields of my struct (seeing what PhantomData I have to determine variance) but other times it's going to only looking at the generics signature. If it is looking at the fields to determine variance why can't it also look at the fields to determine that it doesn't matter whether T is 'static?

If it never looked at the fields I would say, "ok this is to preserve local reasoning, we never look at the fields to avoid endlessly deep iterating through the struct..." but then PhantomData is only useful because apparently it looks through the fields already.

Why do you think it doesn't? fn(T) itself is non-'static when T is not 'static, so looking at the fields leads to the same conclusion.

There is no way in Rust to make a type with an embedded lifetime parameter outlive that lifetime parameter. As I said before, this isn't really related to variance because it has nothing to do with subtyping.

However, you're right that "peering" into types to see how they use generics does run a bit counter to the general trend of avoiding nonlocal reasoning, and it's possible to accidentally over- or under-promise by incorrectly using (or incorrectly not using) PhantomData.

1 Like

Ah, someone on another forum specifically recommended fn(T) to me claiming it is the signature you want if you don't want any variance relationship. But to be fair, I don't understand why what you're saying would be true. Why should the fact that I may have a fn to a function that could take a short lived object change the fn itself to not be static? The object is being taken by move and functions in Rust always live forever, there's no code deallocation.

Yes, the function pointer itself (fn(T)) is 'static.

But perhaps they're just leaving the option open of that not always being the true. Or just haven't special-cased generic function pointers "enough". Adding a bound to fn(T) -> () avoids the error, similar to how constraining T does for PhantomData<T>.

Edit: The bound is not redundant, see following comments.

No, it's actually not.

fn(T): 'static is just a roundabout way of saying T: 'static. There is no way to have one without implying the other.

Could we imagine a version of Rust where this isn't the case, and fn(&'a i32) is 'static? Sure, I can see an argument for that. But it's not the case for the language we have today.

Well, that's not quite true: fn(T) still has variance in T, it's just the opposite kind of variance (contravariant instead of covariant). But variance only affects subtyping; it doesn't have much to do with whether a type is 'static.

You are right, it doesn't have to be true, but it is true. I'm unsure whether this can be changed backwards-compatibly; that seems like a worthwhile discussion to have.

1 Like

I mean...

use core::any::TypeId;
fn type_id_of_val<T: 'static>(_: T) -> TypeId {
    TypeId::of::<T>()
}

fn arg_not_static(_: &str) {}

fn main() {
    println!("{:?}", type_id_of_val(arg_not_static));
}

Perhaps we're talking past each other?

I'm not certain, but this might be because arg_not_static can be coerced/inferred to have type fn(&'static str).

For comparison:

fn foo<'a>() {
    // compiles
    println!("{:?}", TypeId::of::<fn(&'static str)>());
    
    // doesn't compile:
    // println!("{:?}", TypeId::of::<fn(&'a str)>());
}
2 Likes

And this is because fn(&str) = for <'a> fn(&'a str) is subtype of fn(&'static str), isn't it?

Thanks, now I understand. I guess I've never seen a fn constrained within a named lifetime scope before (is there ever a reason to?).

I don't think it had to coerce; it's a for<'a> fn(&'a str). [Technically not this either, it is still associated with the named function in my example. But lifetime-wise.]

@quinedot This does not work with your code;

    fn _foo<'a>(_:&'a i32) {
        let y_tho = Y::<&'a i32>(PhantomData);
        y_tho.example(42);
    }

I have tried

trait Unowned<T> {
    type U: 'static;
}
impl<T> Unowned<T> for T {
    type U = ();
}
struct Y<T>(PhantomData<&'static <T as Unowned<T>>::U>);

but no luck, generics aren't supposed to be bypassed.

Going off topic:
It could be considered an oversight the behaviour of 'fn' with lifetime arguments. (Fix would likely make syntax more complex.) libloading an example; uses unsafe since you can't shorten life.

To clarify more, for<'a> fn(&'a str) is 'static.

2 Likes

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.