If this is meant as a general question as to why the dyn
lifetime is syntactically denoted with + 'a
instead of an added parameter on the trait name, the reason is that dyn Trait + 'a
represents an erased type that meets a Trait + 'a
bound (and dyn Trait + Send + 'a
a Trait + Send + 'a
bound, etc).
If you mean why doesn't that crate in particular rewrite your trait to have an extra lifetime parameter, well,
- there's no need for an extra lifetime in the trait itself
- it would still need the
dyn
lifetime to support type erasing non-'static
types
- the lifetime in the trait parameter would be invariant in
DynTest
- so using the same lifetime for both would be strictly less flexible than not having the lifetime parameter on the trait (as the
dyn
lifetime is covariant)
I look at a few specific examples at the end of this reply.
Lifetimes don't denote liveness scopes. One can think an outlives bound as an approximation of value lifetimes, but personally I feel that still results in more confusion than useful intuition.
At any rate, types are only valid while all their parameters, including lifetimes, are valid. That's true for DynTest<'_>
and also every other type.
The way outlives bounds work is:
SomeType<'a, 'b, C, D>: 'x
if and only if
'a: 'x,
'b: 'x,
C: 'x,
D: 'x
And analogously for dyn
types.
dyn Trait<'a, 'b, C, D> + 'e: 'x
if and only if
'a: 'x,
'b: 'x,
C: 'x,
D: 'x,
'e: 'x
dyn ErasedTest + 'dynosaur_struct: 'dynosaur_struct
is a (trivially) satisfiable bound because of how outlives bounds work.
dyn ErasedTest
on its own still has an elided lifetime. If you wrote this bound down:
where
dyn ErasedTest: 'dynosaur_struct
It is short for
where
dyn ErasedTest + 'static: 'dynosaur_struct
Which is also a trivially satisfiable bound. It also meets a : 'static
bound.
It doesn't mean the erased value lives forever. It means the type of the erased value meets a 'static
bound. Like String
does, say.
(N.b. the elided dyn
lifetime is not always 'static
.)
The borrow checker is a pass-or-fail test that can't change the semantics of a program. You can change which programs are accepted by changing lifetimes in your source code, but you can't change where a value gets destructed. Which is what "give the object a liveness scope" sounds like to me.
Here's some different possibilities and how they differ from what the macro does.
// (A): What you got out of the macro
trait ErasedTest { .. }
struct DynTest<'a> { ptr: dyn Erasedtest + 'a }
// (B): Adding a parameter, using `+ 'static`
trait ErasedTest<'a> { .. }
struct DynTest<'a> { ptr: dyn ErasedTest<'a> /* + 'static */ }
// (C): Adding a parameter and equating it to the `dyn` lifetime
trait ErasedTest<'a> { .. }
struct DynTest<'a> { ptr: dyn ErasedTest<'a> + 'a }
// (D): Adding a distinct parameter
trait ErasedTest<'b> { .. }
struct DynTest<'a, 'b> { ptr: dyn ErasedTest<'b> + 'a }
B
would disallow type-erasing implementors that do not meet a 'static
bound, and would add an unused invariant lifetime parameter to DynTest
(which you'd always want to be 'static
).
C
would be more similar to A
, but the lifetime parameter becomes invariant.
D
would allow the dyn
lifetime to stay covariant, but like B
adds an unused invariant lifetime parameter to DynTest
. DynTest<'a, 'static>
from D
would be equivalent to DynTest<'a>
from A
. The added lifetime parameter serves no purpose, so DynTest<'a, 'static>
would always be what you want.
In all cases, the outlives bounds / validity of the type works as was described above. It doesn't matter where the lifetimes go inside the struct definition.