Reputation: 823
I am hoping to make a little more progress in my understanding of lifetimes and so I'm trying to figure this out. What am I telling the compiler when I write this?
trait MyTrait<'a> {
fn foo(&'a self) -> &'a str;
}
and how is it different from when I write this?
trait MyTrait {
fn foo<'a>(&'a self) -> &'a str;
}
(the difference syntactically is that I moved the declaration of the <'a>
lifetime to the method instead of the trait)
I had thought that in both cases I was simply declaring a lifetime parameter for use in the method signature, but that seems to be very incorrect because the first block produces unexpected error messages.
What I'm trying to express is "the str
returned from foo()
should have the same lifetime as the object implementing MyTrait
". And to that end the second block seems to be the right way to say it. I'm also aware that in this case I can just leave out all the lifetime annotations and just let the compiler infer them, but part of the exercise I'm going through is to try to use explicit lifetime parameters everywhere to test my knowledge of what they mean.
The context where this came up is illustrated in this code, where the first syntax above produces errors, but the second one doesn't:
trait MyTrait<'a> {
fn borrow_contents(&'a self) -> &'a str;
}
struct Foo {
contents: String,
}
impl<'a> MyTrait<'a> for Foo {
fn borrow_contents(&'a self) -> &'a str {
&self.contents
}
}
fn main() {
let boxed_trait_object: Box<dyn MyTrait> = Box::new(Foo {
contents: "first".into(),
});
boxed_trait_object.borrow_contents(); // -> ERROR: borrowed value does not live long enough...
}
Upvotes: 4
Views: 230
Reputation: 71430
Replace the lifetime parameter with a type parameter; is it easier for you to understand now?
trait MyTrait<T> {
fn foo(v: T);
}
// Versus:
trait MyTrait {
fn foo<T>(v: T);
}
In this first snippet, the trait contains the (lifetime or type) parameter. That means the implementor is choosing what the parameter will be. A specific type can have multiple implementations for different parameters (coming from a single generic impl
blocks or from multiple distinct impl
blocks), but each implementation defines its own foo()
, that takes its specified T
(or the lifetime parameter). For example, you can implement MyTrait<u32> for Foo
and MyTrait<u64> for Foo
, with different logic for foo()
.
With the second snippet, the method defines the parameter. That means the caller of the method is choosing what the parameter will be. The method defines a uniform interface and implementation that works with all types (or lifetimes) (satisfying on the constraints specified), and the caller chooses what type to put instead of T
.
Those ways of declaring sometimes lead to the same result, because just like fn foo<T>()
defines a uniform interface and implementation, we can define a generic impl impl<T> MyTrait<T> for Foo
, and that will also force a uniform interface and implementation. However, the discrepancies reveal themselves when we are forced into matching a specific implementation of the trait. Usually we are allowed in each call to call a different implementation, but in the following cases we are forced to pick one implementation only:
for<T>
), we cannot say we take "a type that implements MyTrait<T>
for any T
(U: for<T> MyTrait<T>
), so we are forced to pick one concrete implementation. That means we can call foo()
in the second snippet with any type, since inside one implementation foo()
can take many types, but we are forced to call foo()
in the first snippet with only one type, the type we put in the trait bounds.Self
with the same generic parameters, but not others. So we can call the second snippet with any type (because that is the interface we defined), but the first snippet only with the same T
as in implementing type, i.e. the T
declared in the trait.Your example also fails because of this. By specifying dyn MyTrait
, you actually say dyn MyTrait<'_>
, which means again a specific lifetime and therefore a specific implementation.
These concerns apply more to type parameters and less to lifetime parameters because we have HRTB. Your example can also be fixed with them:
fn main() {
let boxed_trait_object: Box<dyn for<'a> MyTrait<'a>> = Box::new(Foo {
contents: "first".into(),
});
boxed_trait_object.borrow_contents();
}
Still, it is less convenient, and therefore you should prefer specifying the parameters on the method when you can.
It seems the second snippet is always more general, so why we have the first? It is useful in the following cases:
struct Foo<T>(T);
trait MyTrait<T> {
fn get(&self) -> &T;
}
impl<T> MyTrait<T> for Foo<T> {
fn get(&self) -> &T { &self.0 }
}
Here, we cannot implement foo()
for any type, because we only have a single T
we can hand (you may be confused because this is a generic implementation, for any T
; but for each concrete Foo<T>
, we only have one T
we can hand).
Or with lifetimes, perhaps the type contain a reference with some specific lifetime and so cannot implement the trait with any lifetime. For example:
struct Foo<'a>(&'a u32);
trait MyTrait<'a> {
fn into(self) -> &'a u32;
}
impl<'a> MyTrait<'a> for Foo<'a> {
fn into(self) -> &'a u32 { self.0 }
}
Again, here we cannot give any lifetime, only what we have.
Still, the more common case (especially with lifetimes) is to put the parameter on the method, and this is indeed the default: if you omit the lifetime, the elided lifetime will become a hidden parameter on the method. This:
trait MyTrait {
fn foo(&self) -> &str;
}
Is equivalent to this:
trait MyTrait {
fn foo<'a>(&'a self) -> &'a str;
}
Not this:
trait MyTrait<'a> {
fn foo(&'a self) -> &'a str;
}
Upvotes: 6