Idea: enabling lifetime defaults in future editions

Lifetime defaults (e.g. type Ref<'a='static, T> = &'a T) are difficult for the same reason default-aware inference is difficult. In overly short, we don't currently allow lifetime defaults because lifetimes are always inferred if unmentioned, and we don't currently have the capability to both default and infer the same inference variable.

But we now have a way to explicitly request lifetime inference — '_. There's an existing lint to suggest using '_ where elided lifetimes are used (elided_lifetimes_in_paths, but despite being part of the rust_2018_idioms lint group[1], it's still allow-by-default in all editions, due AIUI in major part to disagreement if it's an improvement for things like &fmt::Formatter to add <'_> when it already contains a visibly elided lifetime.

I'd like to propose a middle-ground: given some type Ref<'a, T>, continue to not lint on Ref, but lint on Ref<T>. Specifically, don't lint when the entire generic list is omitted, but lint when the generic list is present but omits any lifetimes. For comparison, consider that a generic list not specifying a slot for a type or const generic is an error, as is specifying any but not all lifetime parameters.

The reason to start linting on implicit lifetime generic elision is that it offers us the potential to change what it means in a future edition. Specifically (as per the title of the thread), I'd like to propose that in a future edition, specifying a generics list which includes a lifetime generic but not providing a lifetime argument uses the default provided lifetime if it exists, and error if there is no default provided (requiring the use of '_). This brings lifetime generics in line with the other generics' elision/inference behavior. Order mixing would still be disallowed. This does pessimize a custom Ref type even further from native references, but undoing that deficit is extremely difficult for minimal gain[2].

In existing editions, the default lifetime would be allowed in definitions (likely with an extra warning) but only impact diagnostics at the use site. e.g. suggest either '_ or 'default in the elision (edition migration) warning and 'default when inference fails (if 'default can be named in the scope, meaning the specified default, not a keyword lifetime). It's technically possible to make specifying one but not all lifetimes in current editions use the defaults for the remaining lifetimes instead of error, but I'd suggest leaving it as an error, prioritizing in-edition consistency over matching future-edition behavior; a question to resolve in the stabilization period.

If we decide we want to do this, around now is the time to start considering it to give appropriate lead time on the warning before edition2024 rides the train.


  1. It still feels backwards to write #[warn(rust_2018_idioms)]... can we rename/alias that lint to something like pre_2018_idioms or rust_2018_nonidioms to fix the polarity here? We did a similar rename for bad_style to nonstandard_style to make #[allow(bad_style)] less needlessly judgemental. ↩ī¸Ž

  2. Field projection has a chance, but at least Deref and Index evaluate to a place, so dereference and index expressions cannot ever produce a custom reference wrapper type. What they could do is produce an associated Deref type (like how in C++ the requirement for operator-> is to return a type with an operator->) with the same target type, which would then itself again be dereferenced (recursively until dereferencing &Self::Target) to compute the place. Temporary lifetime extension rules would then apply for the temporary intermediate impl Deref. Where proper reference semantics are desirable and no guard Drop impl, though, &Cell<T> should be used instead of Ref<T>, and we should continue to (slowly) pursue custom pointee metadata types to make this more achievable. (I'd be ecstatic even just for "dyn const" extra-fat-pointer support as a generalization of array unsizing.) ↩ī¸Ž

2 Likes

It is actually a hard error in async fn already

async fn foo(_: &std::fmt::Formatter) {}
error[E0726]: implicit elided lifetime not allowed here
 --> src/main.rs:2:18
  |
2 | async fn foo(_: &std::fmt::Formatter) {}
  |                  ^^^^^^^^^^^^^^^^^^^ expected lifetime parameter
  |
  = note: assuming a `'static` lifetime...
help: indicate the anonymous lifetime
  |
2 | async fn foo(_: &std::fmt::Formatter<'_>) {}
  |                                     ++++

For more information about this error, try `rustc --explain E0726`.
1 Like

How useful are lifetime defaults? Can you even assign a lifetime other than 'static?

One place I have long wanted it is

pub type BoxFuture<'a = 'static, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

which only needs support for 'static.

It might be possible to use other lifetimes in GATs, e.g. something like

trait Foo<'a> {
  type Bar<'b = 'a>;
}

whether there's actual utility to that :person_shrugging:. Interestingly it looks like you can't have elided lifetimes in associated types so that's already future-compatible.

2 Likes

Due to how lifetimes work, there's three ways in which defaults could be defined:

  • <'a = 'static>, by far the common case.
  • <'a, 'b = 'a>, to combine the benefit of a less lifetime laden API with the ability to split the lifetimes.
  • <'a> <'b = 'a>, for I'm not sure what benefit, perhaps assisting semver-compatible generalization to GAT?
  • Maybe, <'a = '_> to still be inferred if unnamed, as today.

There's some potential issues with this for functions, though, as in a function signature you have a singular '_ named elided lifetime. Notably, switching from only elided lifetimes to using named lifetimes is currently always nonbreaking. (Adding a new named lifetime when some already exist is already breaking IIRC.)

I think of (non inference impacting) defaults in terms of aliases. In short,

type A<'a = 'static, 'b = '_, T = str> = &'b &'a T;

should be like having aliases overloaded for the different generic lists:

type A<'a, 'b, T> = &'b &'a T;
type A<'a, 'b>    = &'b &'a str;
type A<'a, T>     = &'_ &'a T;
type A<'a>        = &'_ &'a str;
type A<T>         = &'_ &'static T;
type A<>          = &'_ &'static str;
type A            = &'_ &'_ str;
4 Likes