Open ldr709 opened 3 years ago
Note that changing the function pointer to take an &'a u32
instead of an &'b u32
compiles successfully. This seems to further indicate that the lifetime of the function is bound by the lifetimes of its arguments.
pub fn foo<'a, T: 'a>() {}
pub fn bar<'a, 'b>() {
foo::<'a, fn(&'a u32)>(); // Passes
}
I don't think this is actually a bug. You've explicitly constrained the lifetime to 'b
, which in fact doesn't outlive 'a
. If you remove 'b
then it works fine: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f2bb3ba99563aec02087e6391130b2f1
Yes, 'b
does not outlive 'a
. But it doesn't make sense for fn(&'b u32)
to be bound by the lifetime 'b
, since it's a function pointer, and lives for the whole execution. There does not seem to be sufficiently detailed documentation for lifetimes to be able to say that it is a bug because the compiler is violating a specification or something like that, so in the absence of such I'm guessing based on the semantics of what lifetimes mean. A fn(&'b u32)
takes a value that only lives for lifetime 'b
, but there's no reason for the function itself to only live for lifetime 'b
.
Oh I may have misread - 'b
is the lifetime of the parameter, not of the function pointer itself. Yeah, this behavior is pretty confusing then.
It appears that the spec for how outlives predicates are handled was defined in RFC 1214, and the code that actually adds the obligations that a function pointer outlives its components is primarily these two functions: https://github.com/rust-lang/rust/blob/6c523a7a0ef121fe97ad6a367a3f3b92f80dc3f0/compiler/rustc_infer/src/traits/util.rs#L182 https://github.com/rust-lang/rust/blob/6c523a7a0ef121fe97ad6a367a3f3b92f80dc3f0/compiler/rustc_middle/src/ty/outlives.rs#L60
The RFC claims its motivation was more to codify and simplify the existing rules (It does explicitly call out this case, but for simplicity, not unsoundness), so I suspect it would be possible to make a new RFC to alter the rules around function pointers, as long as it could show it would not induce unsoundness or break backwards compatibility? I am not incredibly familiar with the RFC process.
The "Simpler outlives relation" section under https://rust-lang.github.io/rfcs/1214-projections-lifetimes-and-wf.html#motivation makes me worried that this might be more subtle than it appears, but I agree that intuitively fn
s sure seem like they're 'static
.
I agree that I don't think this is currently a T-compiler
bug, and needs an RFC to update the rules before anything can change.
It appears that the spec for how outlives predicates are handled was defined in RFC 1214
Thank you for finding where the current rules are specified. When I searched I was unable to find the key terms.
I find the syntactic definition of outlives to be highly counterintuitive, but this also makes me worry that there are many subtleties that I am missing. I do not have the in depth knowledge of Rust's type system needed for telling if changing it would cause problems, let alone for writing an RFC.
I posted a pre-RFC for amending 1214 on the internals forums, after doing a bit of consideration. It proposes relaxing the argument constraint for sure, maybe the return type constraint. Hopefully it will get some commentary.
The traits are a separate issue, as it would add more special casing than what currently exists, and is much more likely to have nuanced edge cases for custom impls.
It would be unsound to simply relax the lifetime of functions to 'static
due to Any::downcast_ref
. In particular, Any
is based on the reasoning that T: 'static
implies that any lifetime parameters on T
have been assigned the 'static
value. This makes it possible to assume that it is a unique type, despite not considering lifetime parameters at all when computing type-id. Otherwise, lifetimes would need to be included in the type-id which isn't implemented in the compiler afaik.
fn cast_to_static(fn_: fn(&'static u8)) -> fn(&'a u8) {
// This *must* not be allowed to work.
(&fn_ as &dyn Any).downcast_ref().unwrap()
}
static CELL: SyncOnceCell<&'static u8> = OnceCell::new();
fn store_static(val: &'static u8) {
CELL.get_or_init(|| val)
}
fn break_it() {
let stack: u8 = 0u8;
cast_from_static(store_static)(&stack)
}
I'm afraid we have to relax one of the two lines of reasoning. Other uses cases have popped up over the time are with regards to "generativity" and such concepts as ghost cells or more generally ghost-like data structures. They commonly define a token type whose main property is being invariant, for which function types are easy to use:
struct InvariantUniqueToken<'a> { inner: PhantomData<fn(&'a()) -> &'a ()> }
As it stands, the usability of these tokens is low because the lifetime bound propagates everywhere. It's not possible to store the token into any data structure requiring 'static
. However, they suffer from the same problem where Any
would allow arbitrary lifetime changes .
This is making it difficult for me to type-erase a trait with a type parameter that's used as an argument for a function not exposed in the type-erased variant of the trait.
The current logic doesn't even seem to be consistent. This compiles:
struct Foo<'a>(fn(&'a i32));
fn assert_static<T: 'static>(x: T) {}
pub fn frob<'a>(x: Foo<'a>) { assert_static(x); }
whereas this does not:
struct Foo<T>(fn(T));
fn assert_static<T: 'static>(x: T) {}
pub fn frob<T>(x: Foo<T>) { assert_static(x); }
This compiles:
struct Foo<'a>(fn(&'a i32)); fn assert_static<T: 'static>(x: T) {} pub fn frob<'a>(x: Foo<'a>) { assert_static(x); }
This works because of contravariance. Foo<'a>
is contravariant in 'a
, and so is a subtype of Foo<'static>
. So what's really happening in fn frob
is this:
pub fn frob<'a>(x: Foo<'a>) {
// subtype → supertype coercion
let supertype: Foo<'static> = x;
assert_static::<Foo<'static>>(supertype);
}
That makes sense, thanks. It's still surprising to me that the same mechanism doesn't apply for fn(T)
, but I guess that's out of scope of this issue.
An arbitrary T
may not have a 'static
subtype, so an arbitrary fn(T)
may have no 'static
supertype it can coerce to. For example *mut &'a i32
has no subtype that is 'static
, so fn(*mut &'a i32)
is ineligible for coercion to a 'static
supertype.
Function pointers have no lifetime of their own, and essentially are
'static
(Unsafe Code Guidelines Reference), so I would expectfn(T) -> U: 'a
to hold for any lifetime'a
and typesT, U
. A function pointer that lived longer than its argument type would be fine, since it won't be possible to make an instance of the argument type after it's expired, and so you cannot call it then. Similarly, there's nothing wrong with a function pointer that lives longer than its return type, as outside the return type's lifetime you either won't be able to call it or it will never return.Yet the following example fails to compile.
Playground
The compiler seems to think that a function pointer is only valid for as long as the types it uses are, rather than for the duration of the program.
I have also run into a similar issue when using
Box<dyn Fn(&'b u32)>
(and alsoBox<dyn Fn(&'b u32) + 'a>
) instead offn(&'b u32)
, but I thought that the function pointer case would be the clearest.