Variance
The dyn Trait
lifetime is covariant, like the outer lifetime of a
reference. This means that whenever it is in a covariant type position,
longer lifetimes can be coerced into shorter lifetimes.
#![allow(unused)] fn main() { trait Trait {} fn why_be_static<'a>(bx: Box<dyn Trait + 'static>) -> Box<dyn Trait + 'a> { bx } }
The trait object with the longer lifetime is a subtype of the trait object with the shorter lifetime, so this is a form of supertype coercion. In the next section, we'll look at another form of trait object subtyping.
The idea behind why trait object lifetimes are covariant is that the lifetime represents the region where it is still valid to call methods on the trait object. Since it's valid to call methods anywhere in that region, it's also valid to restrict the region to some subset of itself -- i.e. to coerce the lifetime to be shorter.
However, it turns out that the dyn Trait
lifetime is even more flexible than
your typical covariant lifetime.
Unsizing coercions in invariant context
Earlier we noted that
you can cast a dyn Trait + 'a
to a dyn Trait + 'b
, where 'a: 'b
.
Well, isn't that just covariance? Not quite -- when we noted this before,
we were talking about an unsizing coercion between two dyn Trait + '_
.
And that coercion can take place even in invariant position. That means
that the dyn Trait
lifetime can act in a covariant-like fashion even in
invariant contexts!
For example, this compiles, even though the dyn Trait
is behind a &mut
:
#![allow(unused)] fn main() { trait Trait {} fn invariant_coercion<'m, 'long: 'short, 'short>( arg: &'m mut (dyn Trait + 'long) ) -> &'m mut (dyn Trait + 'short) { arg } }
But as there are no nested unsizing coercions, this version does not compile:
#![allow(unused)] fn main() { use std::cell::Cell; trait Trait {} // Fails: `Cell<T>` is invariant in `T` and the `dyn Trait` is nested fn foo<'l: 's, 's>(v: Cell<Box<Box<dyn Trait + 'l>>>) -> Cell<Box<Box<dyn Trait + 's>>> { v } }
Because this is an unsizing coercion and not a subtyping coercion, there may be situations where you must make the coercion explicitly, for example with a cast.
#![allow(unused)] fn main() { trait Trait {} // This fails without the `as _` cast. fn foo<'a>(arg: &'a mut Box<dyn Trait + 'static>) -> Option<&'a mut (dyn Trait + 'a)> { true.then(move || arg.as_mut() as _) } }
Why this is actually a critical feature
We'll examine elided lifetime in depth soon, but let us note here how this "ultra-covariance" is very important for making common patterns usably ergonomic.
The signatures of foo
and bar
are effectively the same in the following example:
#![allow(unused)] fn main() { trait Trait {} fn foo(d: &mut dyn Trait) {} fn bar<'a>(d: &'a mut (dyn Trait + 'a)) { foo(d); foo(d); } }
We can call foo
multiple times from bar
by reborrowing
the &'a mut dyn Trait
for shorter than 'a
. But because the trait object
lifetime must match the outer &mut
lifetime in this case, we also have
to coerce dyn Trait + 'a
to that shorter lifetime.
Similar considerations come into play when going between a &mut Box<dyn Trait>
and a &mut dyn Trait
:
#![allow(unused)] fn main() { trait Trait {} fn foo(d: &mut dyn Trait) {} fn bar<'a>(d: &'a mut (dyn Trait + 'a)) { foo(d); foo(d); } fn baz(bx: &mut Box<dyn Trait /* + 'static */>) { // If the trait object lifetime could not "shrink" inside the `&mut`, // we could not make these calls at all foo(&mut **bx); bar(&mut **bx); } }
Here we reborrow **bx
as &'a mut (dyn Trait + 'static)
for some
short-lived 'a
, and then coerce that to a &'a mut (dyn Trait + 'a)
.
Variance in nested context
The supertype coercion of going from dyn Trait + 'a
to dyn Trait + 'b
when 'a: 'b
can happen in deeply nested contexts, provided the trait
object is still in a covariant context. So unlike the Cell
version
above, this version compiles:
#![allow(unused)] fn main() { trait Trait {} fn foo<'l: 's, 's>(v: Vec<Box<Box<dyn Trait + 'l>>>) -> Vec<Box<Box<dyn Trait + 's>>> { v } }