Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Difference in trait object default bounds for early/late bound lifetimes #47078

Open
matthewjasper opened this issue Dec 30, 2017 · 1 comment
Labels
A-lifetimes Area: lifetime related C-bug Category: This is a bug. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@matthewjasper
Copy link
Contributor

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
}
@pietroalbini pietroalbini added the A-lifetimes Area: lifetime related label Jan 30, 2018
@XAMPPRocky XAMPPRocky added T-lang Relevant to the language team, which will review and decide on the PR/issue. C-bug Category: This is a bug. labels Apr 10, 2018
@QuineDot
Copy link

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: 'a bound. 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> for trait Double<'a, b>: 'a + 'b
  • 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 struct bounds (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

'static trait 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;
}

The trait bound imparts an implied bound even when the default trait object isn't the default lifetime, and even when the trait bound is not 'static.

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 'static are 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 impl headers and associated types
    • But '_ acts "normally"
  • etc

However those are probably better tracked in their own issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lifetimes Area: lifetime related C-bug Category: This is a bug. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

4 participants