Should the `+ a` in `-> impl Opaque + 'a` be deprecated

In the following article, Niko describes this example:

fn process<'c, T> {
    context: &'c Context,
    data: Vec<T>,
) -> impl Iterator<Item = ()> + 'c {
    data
        .into_iter()
        .map(|datum| context.process(datum))
}

He explains:

Here the process function applies context.process to each of the elements in data (of type T). Because the return value uses context, it is declared as + 'c. Our real goal here is to allow the return type to use 'c; writing + 'c achieves that goal because 'c now appears in the bound listing. However, while writing + 'c is a convenient way to make 'c appear in the bounds, also means that the hidden type must outlive 'c.

The last clause caught my attention.

'a appear in the bounds, also means that the hidden type must outlive 'a

In type bound terminology, this translates to: "The opaque type cannot contain references shorter than 'c" (i.e., T: 'c).

Revisiting an Earlier Discussion

I posted about this 3-6 months ago and learned a lot from the discussion, even if I didn't convince anyone! After revisiting the discussion and tweaking the claim, I think the following explains the tension that I can't seem to explain away.

The Fundamental Ambiguity of + 'a in Rust 2024

The Core Problem

+ 'a on return types creates the bound T: 'a, which states: "T cannot contain references shorter than 'a."

But the borrow checker already makes this impossible.

Why the Bound Is Nonsensical - there is no useful intent

Single Lifetime Case

fn foo<'a>(x: &'a str) -> impl Display + 'a {
    // What references shorter than 'a not already prevented by the borrow-checker can live here?
}

The bound + 'a enforces a constraint that cannot be violated. It's like writing:

fn add(x: u32, y: u32) -> u32 
    where u32: Copy  // ← Always true, completely redundant

Multi-Lifetime Case

fn foo<'a, 'b>(x: &'a str, y: &'b str) -> impl Display + 'a {
    // The + 'a prevents using y when 'b < 'a
}

Here + 'a does constrain something—but why would you want this?

  • If you don't need 'b, don't use y
  • If you do need both lifetimes, the constraint just gets in your way
  • The bound doesn't prevent bugs; it enforces an arbitrary restriction

The Ambiguity

+ 'a conflates two orthogonal concepts:

  1. Capture: "Make 'a available to the hidden type" (what you actually need)
  2. Bound: "Constrain away shorter lifetimes" (redundant or restrictive)

Before use<>, you had no choice—you needed + 'a for capture.

After use<>, the bound becomes pure noise that:

  • Pretends to add a constraint that's already impossible to violate
  • Or artificially restricts valid implementations
  • Obscures the actual intent (capture)

The Succinct Argument

The world where + 'a has meaning is a world where you could return references shorter than 'a. But the borrow checker makes this world impossible. Therefore, + 'a either enforces nothing (single lifetime) or enforces an arbitrary restriction (multiple lifetimes). In Rust 2024, use<'a> clearly expresses capture without the nonsensical bound semantics.

In my estimation, for new code, + 'lifetime should be considered deprecated in favor of use<>.

What do others think? Am I missing something? Does anyone have an example of where they needed + 'a in the return type, using the 2024 edition?

What's the rule you're envisioning here? That the compiler looks at the body to decide which lifetimes to enforce on the return type?

The outlives bound is exploited by borrow check and can't be removed without breaking API's that utilize that. It is not pure noise.

See this pr for details, or I can cook up some example later.

This may be better suited for IRLO.

I agree that when having to work with old code, you need the + 'a. That is a side effect of a prior flaw or limitation (all part of making great technology). My proposal is limited to new code. It also sets a path for deprecation.

1 Like

I believe use does what you need here without needing + 'a. No?

Okay, here you go: two examples of providing guarantees to downstream by using a + 'lifetime which do not have an alternative fix (e.g. use<>).

I agree for this example it is a useless bound in edition 2024, and removing it on that edition would be an improvement. Though not to the point of warning by default (much less deprecation) -- the bound is also harmless for this example.

But see my user_2:

pub fn example_2<T: Trait>(t: T) -> impl Trait + 'static { ... }

// user_2
let mut local = "Hello".to_owned();
let opaque = krate::example_2(&*local);

To date, T must be captured -- use<> is rejected. So from a type system perspective, a &'non_static str type is captured, even though in this case it is not materially part of the concrete return type. The + 'static is required to convey that T is not materially part of the concrete return type.

The borrow checker makes use of that information: it understands that uses of opaque do not keep local borrowed. But if you remove the bound it does keep local borrowed. (It must -- without the bound, returning a type that materially captures T would be considered a non-breaking change.)

You had a choice,[1] it just wasn't ergonomic. But it is true that + 'a was suggested instead of that trick, even when it had downsides.

There's almost always a trade-off to adding more bounds to the return type. It's more restrictive for the implementor but more flexible for the consumer. My playground demonstrates providing guarantees to the consumer which would be broken if the + 'lifetime were removed.

The examples demonstrate that lifetimes are part of the type system, such as parameters to generic associated types -- the material presence of references is not the only concern. "Capturing" covers the type system concerns, not just the material presence.

So you can return types which are defined in terms of lifetimes which would violate the accompanying bound if they were materially present. Additionally, you cannot opt out of capturing lifetimes that are part of type parameters at all (so far).

It's analagous to how a <T as Trait<U, V, W>>::Assoc: 'lifetime bound may be satisfied even if (T, U, V, W): 'lifetime cannot be satisfied.

Those statements contradict each other.


  1. "the Captures trick" ↩︎

2 Likes

I should have stated, long term deprecation. At some point I imagine we can find ways to no longer support a feature of a prior edition.

Thank you for this example. Your point is made for a slice of what the syntax space space allows - + ’static . Your example demonstrates a need to weaken my claim for the use of 'static. But this in turn may point to another perhaps bigger issue.

I suspect that if we found a way to articulate how using + ‘a other than when input is TraitWithLifetime that we promise isn’t going to be captured, then a version of my claim holds. i.e., articulate what it means that “you can only use `+ 'static”, everything else is noise :))

That said, should all functions include + ‘static ? Said otherwise, should the compiler always just never capture the unnamed lifetimes?

An alternative, but related take: I wonder if using use in the TraitWithLifetime can somehow signal to the compiler what we need. This would require a “future proofing” of the trait specification.

Or said differently, our current way of specifying a trait is insufficient, is under-specified, by definition of your example. You are making a promise only to get the code to compile, not because you can otherwise be deliberate with your design intent. i.e., the meaning of what you are doing should “always be true”.

The only reason you do it is because we can’t prevent capturing lifetimes that might exist in type parameters.

Is it possible that ‘+ static is the only specification that will ever make sense, the only contract that can ever be supported by the owner of the function that returns some opaque type? Is using 'static just “plugging a whole” that in this context should not have existed in the first place?

If so, the solution is to have the compiler use + 'static always.

If we agree up to this point, can we conclude that any user-specified + 'a is nonsense? To defensively prevent what should never be possible in the first place, use ‘static.

This is the “leaky whole” that using static plugs. It exists because, in fact, we have captured an unamed lifetime. This is breaking the rules of what opaque types are supposed to be able to capture.

Thus, static is a precise fix for a well defined case. Would using static all the time plug the whole without preventing other scenarios from working as intended?

I don’t know what the compiler presumes about the lifetime of type parameters, until presented with a contradiction of sorts by your example, where we have to tell the compiler, “no, despite what you see, static is^ the contract”.

^ depending on the compilers original assumption, the word is either “is” or “remains” the contract.

Given the equivalence of “a lifetime never shorter than” and 'static, in this context, it seems to me we can never brake anything using ‘static. This would support having the compiler see it that way too without having the user be explicit.

Even if there is a way to reference the lifetime in the TraitWithLifetime, you would need to use use<..>. My point is I suspect that there is no other sound case for + 'a. There is only the case you presented for which I have made the point.

The concept of editions is that they allow change without breaking backwards compatibility by always being supported. So that would be Rust 2.0.

No, the exact same examples apply when a non-'static lifetime is in play.

You mean, by making use<'a> act like a bound? That would have the same downsides that let to lifetimes being captured by default (applying a bound to other generics unnecessarily).

No, it should be deliberate, and in my case it is. I'm making a promise that the return type meets the stated lifetime bound -- just like I'm making a promise that it meats the stated trait bounds. By making that promise, it's a breaking change to start materially containing the other captured generics. And the type system exploits this, as demonstrated.

If I don't make that promise, it is a non-breaking change to start materially using the other captured generics. So it is not the case that the promise I'm making by including the bound must (or should) always be true.

I do agree that there could be other ways to solve the examples I illustrated in the future, such as TAIT (albeit less ergonomically), being able to omit type generics with use<..>, and perhaps existential lifetimes.

Until we have them, discouraging + 'a is premature. And even if we get them, I still wouldn't agree that deprecation is warranted.

(I also suspect we'll never be able to not capture Self with opaques in traits and thus + 'a will retain some unique capabilities, but it's all hypothesizing about the future at that point.)

On this I don't agree. The examples show how they enable type-erasing coercion to dyn Trait + 'static (and thus to Any and a usable TypeId), and how they enable more lenient borrow checking.

If your argument is that these capabilities are nonsense, please show how to remove the bounds from the playground above in a non-breaking manner.

2 Likes

You have provided a concrete example for why the current use of use requires + 'a to convey a contract for a lifetime that has nothing to do with the captured type parameters.

In the event we had use<'s> in your example, would that express what is going on? As of now this won't compile, but semantically the idea work?

All in all, going back to the comment that triggered all of this, + 'a in return position can't mean the opaque type must outline 'a, that by definition appears computationally unsound (the opaque child type that depends on parent with hidden lifetime, can't outlive parent). What we have had to do along the way is interpret the meaning to be "the opaque type won't contain any references that live longer than 'a".

Perhaps if the compiler did not capture type parameters by default that that might allow a more precise use of use and square everything?

1 Like

Yes, if we could just not capture T in the second example, that would solve it. And I agree it's the better solution in that case.

It doesn't solve the first example, which can also be written so that a generic type is captured but not materially present, instead of or in addition to the lifetime.

That's exactly what the bound means. That's why if an opaque has + 'a, you can coerce the opaque to a dyn Trait + 'a, even if other generics which don't meet the bound are captured.

Quoting Niko's post:

Because the return value uses context, it is declared as + 'c. Our real goal here is to allow the return type to use 'c; writing + 'c achieves that goal because 'c now appears in the bound listing. However, while writing + 'c is a convenient way to make 'c appear in the bounds, also means that the hidden type must outlive 'c.

So I may be missing your point here.

It solves the second example use case, but not the first. I think this is the canonical issue for the second example.

(I don't think we'll not capture type parameters by default; that would require a change over an edition, in the opposite direction that we just went through with lifetimes.[1] And I suspect it's the "wrong default", that is, that most uses want to capture the types like they do the lifetimes.)

Note that many examples in the issue now compile, similar to my second example.

For example this compiles now:

fn foo<T>(_: T) -> impl Iterator<Item = i32> + 'static {
    vec![2].into_iter()
}

fn bar() {
    let input = vec![1];
    let output0 = foo(&input); //output0 should have nothing to do with input
    let output1 = foo(input);
}

(And the bound is required to make it compile.)


Existential lifetimes is a concept which gets thrown around[2] that may (soundly) solve the first use case. I think this[3] is the best introduction I can cite (even though it's a comment in issue 42940).

Would it "square everything" (make + '_ unnecessary)? Maybe, I'm not sure. Here's the thing about that: you should have to opt in somehow. Because let's say I have:

    pub fn example_1<'t, 's, T>(t: &'t T, s: &'s str) -> impl Debug
    where
        T: for<'a> Trait<Displayer<'a>: 'static>,
    {
        PrivWrapper((t.displayer(), s))
    }

Even though I am not materially capturing the 't here, I am allowed to change the implementation so that I do materially capture the 't -- by allowed I mean, it is supposed to be a non-breaking change to downstream, and the compiler acts accordingly. Callers must act like I materially captured the 't. That flexibility for the callee is one of the major selling points of -> impl Trait; the flexibility should not be silently taken away.

When I added + 's, I was effectively making a promise that 't wasn't materially captured. If + 's was to go away, we'd need something to replace the ability to make that promise. Now, maybe that just looks like this:

pub fn example_1<'t, 's, T>(t: &'t T, s: &'s str) -> impl Debug + use<'s, T>
// Allowed now and the compiler infers that it needs to invisibly track some
// existential lifetime (or even not do so, when sound)

(Effectively use<..> now means "materially captured" and the "nameable captures" are invisibly handled by the compiler in a sound fashion somehow :magic_wand:.)

But if it looks like this:

pub fn example_1<'t, 's, T>(t: &'t T, s: &'s str) 
    -> impl Debug + use<'s, T> + exists<'t where T: 't>

I imagine the ecosystem would balk at that.


Even if the nicer looking version works, would I be in favor of discouraging + 'a? I think I'd have to get use to the hypothetical new feature to be completely sure which I felt was better. But I don't see a need to try to aggresively discourage + '_. The outlives bound is a promise about the bounds of the opaque type, just like the trait bounds are.

Understanding the implications of that bound can be nuanced and complex, but that's nothing new in Rust's borrow checking and trait system.


  1. i.e. "capture everything" ↩︎

  2. "region" is another word for "lifetime" ↩︎

  3. including the hackmd ↩︎

1 Like

It's now clear to me that specifying what is captured is orthogonal to specifying a lifetime bound. I also know that I can't infer a bound based on what is captured. So by definition, the two specifications are required (when needed).

I also understand that to coerce/annotate the opaque return value to trait object dyn Trait + 'a we need + 'a in order propagate the bound.

While the use of TAIT might help with the communicating to the compiler what isn't currently possible, I'm going to have to build an intuition for why it might not ever make sense to propagate what is captured. Maybe it's as simple as "in order to coerce to a trait object that captures a lifetime, we must include a bound". Why that rule might be the case, is where I need to develop my intuition.

While I do agree that + 'a is still needed for now, I think these two examples show a misuse of it, needed to somehow recover the correct lifetime bound after a generic type parameter has been incorrectly captured in its entirety. If you had two lifetimes (assuming they can't be merged) captured other than that type then you wouldn't be able to use this + 'a trick to make the code compile. You would need to pick the smaller of the two, but + 'a + 'b would pick the biggest of the two, which is exactly the same issue that led to adding use.

Yes, you can.[1] The first example in particular is about being forced to capture two lifetimes for soundness reasons (see the concern that tanked PR 116040) even though the opaque outlives one of those lifetimes.


  1. Same playground as linked here ↩︎

My point was about a situation where an opaque type is forced to capture 3 lifetimes but outlives the smallest of the first two, ignoring the third.

Okay, but that's not my example. (And I don't see how my example is a misuse of anything. It's analogous how bounds on associated types and GATs work even when you can't normalize them.)