Footnote: Lifetimes on Trait Objects
In chapter 7, we discussed placeholder lifetimes ('_
). We said that
there were three ways you could use them:
- To simplify
impl
blocks - When consuming/returning a type that needs a lifetime
- To write trait objects that contain references.
In the first case, we saw that anonymous lifetimes just simplified what we needed to write.
In the second case, we saw that Rust recommends that we use it, but we don't need to -- lifetime elision will do what we want.
The one case where it looks like lifetime elision should do what we want,
but it actually doesn't unless we use the '_
is the case of trait objects.
This chapter walks through how trait objects and lifetimes work together.
Let's setup a simple example:
trait Bool { fn truthiness(&self) -> bool; } struct True(); impl Bool for True { fn truthiness(&self) -> bool { true } } struct False(); impl Bool for False { fn truthiness(&self) -> bool { false } } fn get_bool(b: bool) -> Box<dyn Bool> { if b == true { Box::new(True()) } else { Box::new(False()) } } fn main() { let my_bool = true; let my_dyn = get_bool(my_bool); println!("{}", my_dyn.truthiness()); }
To be clear, what we are doing here is creating two structs which represent
true
and false
. They both implement the Bool
trait, which has
the truthiness
function which returns true
or false
.
The get_bool
function returns a Boxed Bool
trait object, based on whether
get_bool
is passed true
or false
.
It's important to realise that since trait objects might or might not contain a reference (or any number of references), all trait objects have lifetimes. This is true, even if no implementors of the trait contain references.1
So, since we need to associate a lifetime with our trait object, we might
think we could rely on lifetime elision. But how would lifetime elision
work for our get_bool
function? There are no input references, so what
output lifetime should we give the trait object? Lifetime elision can't help
us here.
So, in RFC 599 and in RFC 1156, the rules for trait object lifetimes were changed.
The rules are complex, and best outlined in the reference,
but in the case of get_bool
, it means that the lifetime inferred for dyn Bool
is
'static
.
Let's change the example slightly now, such that the struct contains a reference to a bool:
trait Bool {
fn truthiness(&self) -> bool;
}
// CHANGE 1: added &'a bool here
struct True<'a>(&'a bool);
impl<'a> Bool for True<'a> {
fn truthiness(&self) -> bool {
true
}
}
// CHANGE 2: added &'a bool here
struct False<'a>(&'a bool);
impl<'a> Bool for False<'a> {
fn truthiness(&self) -> bool {
false
}
}
fn get_bool(b: &bool) -> Box<dyn Bool> {
if *b == true {
Box::new(True(b))
} else {
Box::new(False(b))
}
}
// CHANGE 3: Update the
fn main() {
let my_dyn = {
let my_bool = true;
get_bool(&my_bool)
// my_bool is dropped here, so the trait object we're returning
// has a dangling reference.
};
println!("{}", my_dyn.truthiness());
}
Now, we get an error:
error: lifetime may not live long enough
--> src/main.rs:22:5
|
21 | fn get_bool(b: &bool) -> Box<dyn Bool> {
| - let's call the lifetime of this reference `'1`
22 | / if *b == true {
23 | | Box::new(True(b))
24 | | } else {
25 | | Box::new(False(b))
26 | | }
| |_____^ returning this value requires that `'1` must outlive `'static`
|
help: to declare that the trait object captures data from argument `b`, you can add an explicit `'_` lifetime bound
|
21 | fn get_bool(b: &bool) -> Box<dyn Bool + '_> {
| ++++
error: could not compile __ due to previous error
Even though lifetime elision means that get_bool
should end up with a
signature like fn get_bool<'elided>(b: &'elided bool) -> Box<dyn Bool + 'elided>
, it doesn't. The special rules for trait objects mean that the
lifetime is: fn get_bool<'elided>(b: &'elided bool) -> Box<dyn Bool + 'static>
. That 'static
bound is incorrect.
Therefore, we need the '_
bound (as this error message tells us) to inform Rust that it
should use the normal lifetime elision rules; rather than the special trait
object rules.
1: https://doc.rust-lang.org/reference/types/trait-object.html#trait-object-lifetime-bounds