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

Default trait object lifetimes section is quite inaccurate #1407

Open
QuineDot opened this issue Sep 26, 2023 · 2 comments
Open

Default trait object lifetimes section is quite inaccurate #1407

QuineDot opened this issue Sep 26, 2023 · 2 comments

Comments

@QuineDot
Copy link

I wrote a lot about this here, but I will recreate the demonstrations in this issue.

Some familiarity with how type bounds work with the default lifetime is important to understand this issue. In particular, references do have the type bound.

// Notionally
builtintype<'a, T: 'a + ?Sized> &'a T;

Factual corrections

&dyn Trait default lifetime can be inferred in expressions

The reference says the default is only inferred in expressions if there are no type bounds and the default outside of expressions is 'static, but this is not true.

Example

This example compiles. Playground.

trait Trait {}
impl Trait for () {}
fn example() {
    let local = ();

    // The outer reference lifetime cannot be `'static`...
    let obj: &dyn Trait = &local;

    // Yet the `dyn Trait` lifetime is!  Thus the default must be inferred
    let _: &(dyn Trait + 'static) = obj;
}

I believe it is due to this change (accepted without FCP).

Note, however, that if you annotate a named lifetime for the generic type (here the reference lifetime), then the default is that named lifetime. This is the behavior the reference currently says always happens.

Example

This example does not compile. Playground.

trait Trait {}
impl Trait for () {}

fn example<'a>(arg: &'a ()) {
    let dt: &'a dyn Trait = arg;
    // fails
    let _: &(dyn Trait + 'static) = dt;
}

The default lifetime for types with multiple bounds can be inferred in expressions

The reference says that for types with multiple bounds, an explicit bound must be specified. However, this is not true in expressions.

Example

This example compiles. Playground.

trait Trait {}
impl Trait for () {}
struct Weird<'a, 'b, T: 'a + 'b + ?Sized>(&'a T, &'b T);

fn example<'a, 'b>() {
    // Either of `dyn Trait + 'a` or `dyn Trait + 'b` is an error,
    // so the `dyn Trait` lifetime must be inferred independently
    // from `'a` and `'b`
    let _: Weird<'a, 'b, dyn Trait> = Weird(&(), &());
}

Trait bounds can override type bounds

According to the reference, trait bounds aren't considered for the default lifetime if the type has bounds. That is, according to the reference, type bounds override trait bounds. However, this is not true: trait bounds can override type bounds.

(When exactly this happens is complicated; see further below.)

Examples

This example compiles. Playground.

pub trait LifetimeTrait<'a>: 'a {}
impl LifetimeTrait<'_> for () {}

// n.b. `'a` is invariant due to being a trait parameter
fn fp<'a>(t: &(dyn LifetimeTrait<'a> + 'a)) {
    f(t);
}

// The fact that `fp` compiled means that the trait bound does in fact apply,
//  and results in a trait object lifetime independent of the reference lifetime.
//
// That is, the type is not `&'r (dyn LifetimeTrait<'a> + 'r)`; 
// the trait bound applied despite the presence of a type bound
pub fn f<'a: 'a>(_: &dyn LifetimeTrait<'a>) {}

This example compiles, but fails with the commented change. Playground.

use core::marker::PhantomData;

// Remove `: 'a` to see the compile error
pub trait LifetimeTrait<'a>: 'a {}

// This type has bounds; moreover, according to the reference, an explicit
// trait object lifetime is required due to having multiple bounds.
pub struct Over<'a, T: 'a + 'static + ?Sized>(&'a T);

pub struct Invariant<T: ?Sized>(*mut PhantomData<T>);
unsafe impl<T: ?Sized> Sync for Invariant<T> {}

// Here, the `'_` is `'static`.  As this compiles, the trait bound must be
// overriding the ambiguous type bounds and making the default
// trait object lifetime `'static` as well.
//
// When the trait bound is removed, an explicit lifetime is indeed required.
pub static OS: Invariant<Over<'_, dyn LifetimeTrait>> = Invariant(std::ptr::null_mut());

'static trait bounds always override type bounds

As a special case of trait bounds overriding type bounds, a 'static trait bound always applies.

Example

This example compiles. Playground.

use std::any::Any;

// n.b. `Any` has a `'static` trait bound
fn example(r: &dyn Any) {
    let _r: &(dyn Any + 'static) = r;
}

This is true even if there are multiple lifetime bounds and one of them is 'static (in contrast with the analogous case for type bounds, which remains ambiguous).

A single trait bound is not always the default

According to the reference, if the type has no lifetime bound and the trait has a single lifetime bound, the default trait object lifetime is the bound on the trait. This is true if the bound is 'static, as covered above. However, it is not always true for a non-'static bound.

(Again, when it is or isn't true -- when trait bounds "apply" or not -- is complicated).

Example

This example compiles. Playground.

trait Single<'a>: 'a {}

// `Box` has no type lifetime bounds, so according to the reference the
// default lifetime should be `'a`.  But instead it is `'static`.
fn foo<'a>(s: Box<dyn Single<'a>>) {
    let s: Box<dyn Single<'a> + 'static> = s;
}

The default depends on lifetimes being early or late bound

According to the reference, the default lifetime only depends on the presence or absence of bounds on type and traits. However, the presence or absence of bounds on function lifetime parameters also plays a role. Namely, early-bound lifetime parameters (lifetimes involved in a where clause) act differently than late-bound lifetime parameters.

Example

This example does not compile. Playground. However, the only change from the last is making the lifetime parameter 'a early-bound.

trait Single<'a>: 'a {}

// Unlike the last example, the trait bound now applies and the
// default trait object lifetime is `'a`, causing a compiler error.
fn foo<'a: 'a>(s: Box<dyn Single<'a>>) {
    let s: Box<dyn Single<'a> + 'static> = s;
}

This behavior has been known about for some time. Note that the linked issue was cited when the current reference material was written; I don't know why the information wasn't included in the reference at that time.

The default lifetime can override the wildcard '_ notation

According to the reference, using '_ in place of elision restores "the usual elision rules". However, this is not always true: trait bounds override both type bounds and the wildcard '_ lifetime in function bodies.

Examples

This example does not compile. Playground.

trait Single<'a>: 'a {}

fn foo<'a>(bx: Box<dyn Single<'a> + 'static>) {
    // The trait bound applies and the default lifetime is `'a`
    let bx: Box<dyn Single<'a> + '_> = bx;
    // So this fails
    let _: Box<dyn Single<'a> + 'static> = bx;
}

This example does not compile. Playground.

trait Single<'a>: 'a {}

fn foo<'a>(rf: &(dyn Single<'a> + 'static)) {
    // The trait bound applies and the default lifetime is `'a`
    let a: &(dyn Single<'a> + '_) = rf;
    // So this succeeds
    let _: &(dyn Single<'a> + 'a) = a;
    // And this fails
    let _: &(dyn Single<'a> + 'static) = a;
}

This example does not compile. Playground. The only difference from the last example is making the reference lifetime explicit. (As was noted above, this changes the default lifetime when there is no trait bound; this demonstrates that the trait bound still overrides the type bound in this scenario.)

trait Single<'a>: 'a {}

fn foo<'r, 'a>(rf: &'r (dyn Single<'a> + 'static)) {
    // The trait bound applies and the default lifetime is `'a`
    let a: &'r (dyn Single<'a> + '_) = rf;
    // So this succeeds
    let _: &'r (dyn Single<'a> + 'a) = a;
    // And this fails
    let _: &'r (dyn Single<'a> + 'static) = a;
}

Other underdocumented behavior of note

I don't think the current reference material contradicts these observations, but it doesn't point them out either.

Trait bounds introduce implied bounds on the trait object lifetime

Similar to how &'a &'b () introduces an implied 'b: 'a bound, trait Trait<'b>: 'b introduces an implied 'b: 'a bound on dyn Trait<'b> + 'a. (The bound is always present, e.g. even if the trait object lifetime is elided.)

Examples

This example compiles. Playground.

pub trait LifetimeTrait<'a, 'b>: 'a {}

fn fp<'a, 'b, 'c>(t: Box<dyn LifetimeTrait<'a, 'b> + 'c>) {
    // This compiles which indicates an implied `'c: 'a` bound
    let c: &'c [()] = &[];
    let _: &'a [()] = c;
}

This example does not compile. Playground.

pub trait LifetimeTrait<'a, 'b>: 'a {}

pub fn f<'b>(_: Box<dyn LifetimeTrait<'_, 'b> + 'b>) {}

fn fp<'a, 'b, 'c>(t: Box<dyn LifetimeTrait<'a, 'b> + 'c>) {
    let c: &'c [()] = &[];

    // This fails, demonstrating that `'c: 'b` is not implied
    // (i.e. the implied bound is on the trait object lifetime only, and
    // not on the other parameters.)
    let _: &'b [()] = c;

    // This fails as it requires `'c: 'b` and `'b: 'a`
    f(t);
}

Type bounds of aliases take precedence

That's to say, if you have an alias without the lifetime bound, it will act like Box<T> (default is often 'static) even if the underlying type had the bound. And if you have an alias with the lifetime bound, it will act like &T (default is often the bound lifetime) even if the underlying type did not have the bound.

Examples

This example compiles. Playground.

trait Trait {}

// Without the `T: 'a` bound, the default trait object lifetime
// for this alias is `'static`
type MyRef<'a, T> = &'a T;

// So this compiles
fn foo(mr: MyRef<'_, dyn Trait>) -> &(dyn Trait + 'static) {
   mr
}

This example does not compile. Playground.

trait Trait {}

// Without the `T: 'a` bound, the default trait object lifetime
// for this alias is `'static`
type MyRef<'a, T> = &'a T;

// With the `T: 'a` bound, the default trait object lifetime for
// this alias is the lifetime parameter
type MyOtherRef<'a, T: 'a> = MyRef<'a, T>;

// So this does not compile
fn bar(mr: MyOtherRef<'_, dyn Trait>) -> &(dyn Trait + 'static) {
   mr
}

See issue #100270.

Bounds on associated types and GATs don't change the default

This is true when the bounds are placed on the associated type or GAT itself.

Example

This example does not compile. Playground.

trait Trait {}
impl Trait for () {}

trait BoundedAssoc<'x> {
    type BA: 'x + ?Sized;
}

// Still `dyn Trait + 'static`
impl<'x> BoundedAssoc<'x> for () {
    type BA = dyn Trait;
}

// Fails as `'a` might not be `'static`
fn bib1<'a>(obj: Box<dyn Trait + 'a>) {
    let obj: Box< <() as BoundedAssoc<'a>>::BA > = obj;
}

trait BoundedAssocGat {
    type BA<'x>: 'x + ?Sized;
}

// Still `dyn Trait + 'static`
impl BoundedAssocGat for () {
    type BA<'x> = dyn Trait;
}


// Fails as `'a` might not be `'static`
fn bib2<'a>(obj: Box<dyn Trait + 'a>) {
    let obj: Box< <() as BoundedAssocGat>::BA::<'a> > = obj;
}

However, it is also true for GATs with bound type parameters.

Example

This example does not compile. Playground.

trait Outer {
    type Ty<'a, T: ?Sized + 'a>;
}
impl Outer for () {
    type Ty<'a, T: ?Sized + 'a> = &'a T;
}
trait Inner {}

// The parameter resolves to `&'r (dyn Inner + 'static)`
fn g<'r>(_: <() as Outer>::Ty<'r, dyn Inner>) {}

// So this fails
fn f<'r>(x: <() as Outer>::Ty<'r, dyn Inner + 'r>) { g(x) }

This latter concern is tracked in #115379 (which is also where the example is from).

Overview of the actual behavior

Type bounds always apply if there are no trait bounds, but also in other circumstances we'll cover below.

When type bounds apply:

  • The wildcard '_ always restores the "normal" elision rules
  • If there are 0 lifetime bounds
    • The default is inferred in expressions
    • The default is 'static everywhere else
    • Example: Box<T>
  • If there is 1 lifetime bound, 'a
    • The default is usually inferred in expressions
      • Exception: If 'a is explicitly annotated in an expression, the default is 'a
    • The default is 'a everywhere else
    • Examples: &T, Ref<'a, T>
  • If there are multiple lifetime bounds (even if one of them is 'static)
    • The default is inferred in expressions
    • The default is considered ambiguous everywhere else and must be explicitly annotated instead

When there are trait bounds, the bounds always implicitly apply to the trait object lifetime, whether that bound is elided, the wildcard '_, or explicitly annotated. In particular, if the trait has a 'static bound, the trait object lifetime is effectively always 'static. This is considered unambiguous even if there are other lifetime bounds (in contrast with type bounds).

Therefore, when a trait has a 'static bound, irregardless of anything else

  • The default is effectively 'static (even when technically inferred or another lifetime parameter)

From here on, we assume any trait bounds are non-'static.

When there are trait bounds, they usually apply fully. The exception is function signatures, which we'll cover separately.

When trait bounds are present and apply fully:

  • Type bounds do not apply
  • The wildcard '_ usually restores the "normal" elision rules
    • Exception: In expressions, '_ acts like full elision
    • The trait bounds still imply bounds on the anonymous lifetime
  • If there is 1 lifetime bound, 'a
    • The default lifetime is 'a
  • If there are multiple lifetime bounds
    • The default is considered ambiguous and must be explicitly annotated instead

In function signatures when trait bounds are present, trait bounds may apply fully, partially, or not at all (falling back to type bounds). I've written up the detailed behavior in #47078; in summary:

  • 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 type bounds apply instead

When trait bounds partially apply, interaction with the inferred bounds from the trait (which are always in effect) can create surprising behavior, as explored in the linked issue.

Other behavior of note:

  • Bounds or lack of bounds on type aliases take precedence
  • Bounds on associated types and GATs have no effect
  • Bounds on GAT type parameters have no effect
@fmease
Copy link
Member

fmease commented Sep 26, 2023

I'm going double-check every single snippet you posted and reply with my findings shortly.

@fmease
Copy link
Member

fmease commented Sep 26, 2023

&dyn Trait default lifetime can be inferred in expressions

  • I can confirm that “[…] the default must be inferred” is true by looking at the debug log of rustc (RUSTC_LOG=debug):
    • &dyn Trait is indeed treated as &(dyn Trait + '_) in that code: astconv::object_safety trait_object_type: dyn [Binder(Trait(Trait), [])] + '?1 (where '?1 is a region inference variable)
    • ?'1 is later constrained to be 'static due to the coercion point: make_eqregion: unifying '?1 with ReStatic, '?1, opportunistically resolved to ReStatic
  • Reason: In resolve_object_lifetime_default, we can't extract the object region from ObjectLifetimeDefault since the anonymous lifetime of the reference wasn't resolved to a ResolvedArg, it's None (that might happen with all anon lifetimes?). Therefore the hir::Lifetime corresp. to the OLD (object lifetime default) gets resolved during astconv instead which maps those "unresolved" lifetimes to a new region var in function re_infer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants