What's lifetime of the value with opaque type?

Consider this example

fn ret<'a>(v: &'a A) -> impl Sized /* + Copy #1*/ {
    0
}
struct A;
fn main() {
    let a = A;
    let r = ret(&a);
    //drop(r);  // #2
    a;
    //r;
}

In this example, the compiler complains that

error[E0505]: cannot move out of `a` because it is borrowed
  --> src/main.rs:9:5
   |
6  |     let a = A;
   |         - binding `a` declared here
7  |     let r = ret(&a);
   |                 -- borrow of `a` occurs here
8  |     //drop(r);  // #2
9  |     a;
   |     ^ move out of `a` occurs here
10 |     //r;
11 | }
   | - borrow might be used here, when `r` is dropped and runs the destructor for type `impl Sized`
   |
note: this call may capture more lifetimes than intended, because Rust 2024 has adjusted the `impl Trait` lifetime capture rules

Either uncomment #1 or #2 will eliminate the compiled error. According to lifetime-capture-rules-2024

We want to promise that the opaque type captures some lifetime 'a, and consequently, that for the opaque type to outlive some other lifetime, 'a must outlive that other lifetime. If we could say in Rust that a lifetime must outlive a type, we would say that the 'a lifetime must outlive the returned opaque type.

In this example, according to the new lifetime-capture rules, the returned opaque type impl Sized captures the lifetime 'a, which would require the requirement that 'a: OpaqueType. So, the requirement means whenever the returned OpaqueType lives, 'a must be in the scope.

Q1:

how to understand what capture lifetimes mean here? Does it mean the opaque type would behave as if it had a field of a type with the captured lifetimes?

Q2:

I don't know whether it is the correct way to say that the type without lifetime parameters has a lifetime, for example, the opaque type here is actually i32, which can have a lifetime.

Q3:

Is the non-lexical lifetime of an opaque type assumed as if it had implemented Drop, such that the value lives until the scope exits? The extra +Copy suppresses the assumption and makes it have the non-lexical lifetime?

You want

fn ret<'a>(v: &'a A) -> impl Sized + use<> {
    0
}

to indicate to the compiler (and to callers) that the returned valued does not capture any of the provided lifetimes.

i32 does in fact not capture any lifetimes at all, but the impl Sized return type has to be forwards-compatible with code you might write in the future, including changes to the body of ret. There are a multitude of relevant types you could return that satisfy impl Sized, which capture lifetimes - not least of all &'a A, which you have a value for right there in v.

Yes, I know this solution. What I ask is labeled with QNumber.

So, how to understand what capture lifetimes mean here? Does it mean the opaque type would behave as if it had a field of a type with the captured lifetimes?

That doesn't have a meaning in Rust. They mean that to satisfy OpaqueType: 'b, they require 'a: 'b.

(I don't know if this side note matters for your questions.)

It usually acts like that. More accurately, it acts like a GAT which is parameterized by the lifetime and can't be normalized.

Which cannot have a lifetime, you mean?

Anyway, you can have type Gat<'a> = i32 too. The opaque alias has a lifetime parameter, the aliased type may or may not.

Yes, what is going on here is that the opaque is assumed to have a destructor that examines the lifetime. (And it is considered a non-breaking change to make that actually true.) It's that notional destructor that keeps the borrow (the lifetime) alive and conflicts with the move of a.

Copy types can't have destructors (#1). Or you can unconditionally change where the destruction happens (#2).

Or don't capture the lifetime.

Or add a lifetime bound to the opaque that outlives the capture (e.g. 'static) and sometimes that can allow uses of the opaque value which do not keep the borrow alive.

No, I meant which can have a lifetime, however, the emphasized lifetime doesn't refer to lifetime parameters, but the range of the scope in which the value lives, as if the value had a lifetime parameter 'b that specifies the range in which 'b is valid.

@quinedot For example, the returned value is of type i32. However, without the extra trait bound + Copy, the impl Sized would behave the same as if it had the destructor, so the returned value will live until its destructor is reached. So, conceptually, the OpaqueType contains a lifetime parameter 'b that specifies it's valid between where it is introduced and where it is destroyed, and capturing lifetime 'a will require 'a:'b, is this a correct understanding?

1 Like

The opaue has a destructor that can observe the captured lifetimes as far as main is concerned, so the destructor keeps the lifetime and thus the borrow alive. The borrow being alive conflicts with the move.

I'd say it conceptually contains all captured generics in the general case.

Lifetimes don't specify -- dictate -- the semantics of valid programs. I suspect we have a bit different mental models, but can't nail down the particulars.[1] Might just be semantics.


  1. Though I do make a point to not conflate Rust lifetimes and liveness scopes. ↩︎

Oh, I see your point. You meant, the OpaqueType capturing the lifetimes and other generic parameters behaves as if the OpaqueType were composed from them, for example, OpaqueType<'a, T, U,...>, so, whenever OpaqueType<'a, T, U,...> is valid the corresponding 'a should also be valid at that point. If we requires OpaqueType<'a, T, U,...>:'b, it also imposes that 'a:'b.

3 Likes

There are some nuances, but pretty much, yes.