Difference in trait object default bounds for early/late bound lifetimes
It looks like early- and late-bound lifetimes behave differently in default object bounds:
trait A<'a>: 'a {}
pub fn foo<'a>(x: Box<A<'a>>) -> Box<A<'a> + 'static> // Infers 'static
{
x
}
pub fn bar<'a>(x: Box<A<'a> + 'a>) -> Box<A<'a>> // Infers 'a
where 'a: 'a
{
x
}
I did some exploration around this issue and wanted to write up the behavior I observed.
First, note that the current reference description of trait object default bounds is not correct. My testing indicates that when a trait bound applies, it overrides the bounds on struct type parameters, not the other way around (as one relevant example).
The tricky part is knowing when the trait bound applies, which is this issue is about (for function signatures).
Second, note that lifetime bounds on traits introduce an implied bound on the trait object lifetime, similar to how the presence of a
&'a &'b ()introduces an implied'b: 'abound. However, this is not the same as the bound being the default trait object lifetime itself.Given those notes, let us call a lifetime parameter of a trait which is also a bound of the trait a "bounding parameter". My observations on the behavior of the default trait object lifetime in function signatures is as follows:
- if any trait bound is
'static, the default lifetime is'static - if any bounding parameter is explicitly
'static, the default lifetime is'static - if exactly one bounding parameter is early-bound, the default lifetime is that lifetime
- including if it is in multiple positions, such as
dyn Double<'a, 'a>fortrait Double<'a, b>: 'a + 'b
- including if it is in multiple positions, such as
- if more than one bounding parameter is early-bound, the default lifetime is ambiguous
- if no bounding parameters are early-bound, the default lifetime depends on the
structbounds (the same as they do for a trait without bounds)
The "one bounding parameter" rule is particular surprising in the case of traits with multiple bounds, due to the interaction with the implied bounds:
trait Double<'a, 'b>: 'a + 'b {} fn h<'a, 'b, T>(bx: Box<dyn Double<'a, 'b>>, t: &'a T) where &'a T: Send, // this makes `'a` early-bound { // `bx` is `Box<dyn Double<'a, 'b> + 'a>` as per the rules above, // so this does not compile: //let _: Box<dyn Double<'a, 'b> + 'static> = bx; // However, the implied bounds still apply, which means: // - `'a: 'a + 'b` // - So `'a: 'b` // // Which is why this can compile even though that bound // is not declared anywhere! let t: &'b T = t; // The lifetimes are still not the same, so this fails //let _: &'a T = t; }More Examples
'statictrait bound overrides&_default and provides an implied bound on the trait object lifetime.use core::any::Any; fn f(d: &dyn Any) { // This a `&(dyn Any + 'static)`... let _: &(dyn Any + 'static) = d; // ...but this fails, so it's not a `&'static (dyn Any + 'static)`. // Thus the trait bound is overriding the `&_` default trait object lifetime // let _: &'static _ = d; } // Here we explicitly force the "normal" elision defaults for `&_`. fn g<'a>(d: &'a (dyn Any + 'a)) { // This is a `&'static (dyn Any + 'static)` because there is an implied // `'a: 'static` bound, and we have forced the inner and outer lifetimes // to be the same in the function signatures. let _: &'static (dyn Any + 'static) = d; }
trait Single<'s>: 's {} // This has "normal" `&_` elision, that is, the reference lifetime and the // trait object lifetime are the same. fn f<'r, 's>(d: &'r dyn Single<'s>) { // But there is still an implied `'r: 's` bound from the trait bound let _: &'s (dyn Single<'s> + 'r) = d; }Due to the other implied bound
's: 'r, all lifetimes are actually the same in the above example.
The requirement that exactly one of the bounded parameters is early-bound or that any of them are
'staticare syntactical requirements, rather than semantic ones. For example:pub trait Double<'a, 'b>: 'a + 'b {} // Semantically, `'a` and `'b` must be `'static`. However the // parameters were not explicitly `'static` and thus this // trait object lifetime is considered ambiguous (even though, // due to the implied bounds, it must be `'static` too). fn foo<'a: 'static, 'b: 'static>(d: Box<dyn Double<'a, 'b>>) {} // Semantically, `'a` and `'b` must be the same. They are also // early-bound parameters due to the bounds. However the parameters // are not syntatically the same lifetime and thus the trait // object lifetime is considered ambiguous. fn bar<'a: 'b, 'b: 'a>(d: &dyn Double<'a, 'b>) {} /* But if you change either example to `Double<'a, 'a>`, then exactly one of the bounded parameters is early-bound, and they will compile: */ fn foo2<'a: 'static, 'b: 'static>(d: Box<dyn Double<'a, 'a>>) {} fn bar2<'a: 'b, 'b: 'a>(d: &dyn Double<'a, 'a>) {}
The wildcard lifetime
'_still acts "like normal", introducing an independent lifetime parameter for input arguments for example.trait Double<'a, 'b>: 'a + 'b {} // Here in the signature, `'_` acts like "normal" and creates an // independent lifetime for the trait object lifetime; let us call // it `'c`. Though independent, it is related due to the implied // bounds: `'c: 'a + 'b` fn foo<'a: 'a, 'b>(bx: Box<dyn Double<'a, 'b> + '_>) { // This fails as `'a: 'b` is required // It succeeds if `+ '_` is removed from the signature // let _: Box<dyn Double<'a, 'b> + 'a> = bx; // This fails as `'b: 'a` is required // It still fails if `+ '_` is removed from the signature // let _: Box<dyn Double<'a, 'b> + 'b> = bx; // This fails as the `'_` lifetime may not be `'static` // It still fails if `+ '_` is removed from the signature, as in that case // the elided lifetime is `'a` and `'a: 'static` would be required // let _: Box<dyn Double<'a, 'b> + 'static> = bx; }Function bodies and other contexts
The exploration above was for function signatures in particular. The behavior is different elsewhere:
- Trait bounds always apply in function bodies
- Whether or not any bounding parameters are early or late bound in the signature
- The wildcard
'_is also overrode in this context
- Trait bounds always seem to apply in
implheaders and associated types- But
'_acts "normally"
- But
- etc
However those are probably better tracked in their own issues.
QuineDot at 2023-07-15 06:49:09
- if any trait bound is