'_ is usually the same as elision. The main exception is when you have a dyn Trait. In that case, full elision often means dyn Trait + 'static or some other lifetime present in the data type, and using '_ (dyn Trait + '_) makes it act like "normal elision".
Outside of dyn, '_ is particularly useful for cases like
fn method(&self) -> SomeLifetimeParameterizedType<'_>
You use to be able to write that like so without warning:
fn method(&self) -> SomeLifetimeParameterizedType
Nowadays you get a warning, because knowing when a borrow is transferred from &self to the return type is usually critical information that shouldn't be completely hidden.
You have to give them names in data structures.
You should give them names in function signatures when elision isn't correct or sufficient. The canonical "isn't sufficient" case is when you need some lifetimes to be the same:
fn longer<'s>(a: &'s str, b: &'s str) -> &'s str {
if a.len() >= b.len() { a } else { b }
}
A case where elision is incorrect could be something like
impl S {
fn method<'s>(&self, s: &'s str) -> Whatever<'s> {
// ...
}
}
where the borrow in the return type is related to s and not &self.
You can think of the lifetime here as the duration of a borrow. buffer is an exclusive borrow of some slice of data for that duration. Using the FooBuffer will keep the exclusive borrow of *buffer active. So long as a FooBuffer is usable, the buffer within will also be usable; assuming no UB from unsafe, the compiler prevents things like dangling &mut at compile time.
But so long as *buffer is exclusively borrowed, you can't use it directly, or you'll get a borrow checker error.
Despite the unfortunate naming, Rust lifetimes -- 'a things -- do not directly denote when values are destructed (which is also commonly called a value's "lifetime"). That said, destructing ("dropping") a value is a use that's incompatible with being borrowed.
It's using numbers to denote elided lifetimes. Sometimes naming the lifetimes yourself makes error messages more clear.... provided you don't change the semantics of the code accidentally when doing so. (I.e. you have to understand the elision rules when doing this.)
You need the two lifetimes to be the same, like you need the types to be the same here:
struct FooOwner<T> {
foo: Vec<T>
}
impl<T> FooOwner<T> {
pub fn new(foo: Vec<T>) -> Self {
Self { foo }
}
}
- It wouldn't make sense to assign a
Vec<i32> when you expected a Vec<String>.
- It wouldn't make sense to assign a
Vec<U> when you expected a Vec<T> (unless U = T).
- Okay, write some code so that I assign a
Vec<T> when I expect a Vec<T> by using T in both places.
Pretty much the same thing is going on with 'b.
You can shorten the borrow duration of a &mut [u8] -- a &'long mut [u8] can coerce to a &'short mut [u8]. But you cannot coerce an arbitrary &'lt mut [u8] to a specific &'b mut [u8]. That's why this does not work:
impl<'b> FooBuffer<'b> {
// Within this impl block we're working with a particular `'b`.
// Here you introduce a new, *independent* lifetime `'b2`.
pub fn new<'b2>(buf: &'b2 mut [u8]) -> Self {
// You get an outlives error because `&'b2 mut [u8]` cannot coerce
// to `&'b mut [u8]`.
Self {
cursor: 0,
buffer: buf,
}
}
}
Which would be semantically the same as this attempt.
// `impl ... S<'_>` is like `impl<'b> ... S<'b>` (but the lifetime isn't nameable)
impl FooBuffer<'_> {
// Elision here introduces a new independent lifetime.
pub fn new(buf: &mut [u8]) -> Self {
This version technically works as well:
impl<'b> FooBuffer<'b> {
pub fn new<'long: 'b>(buf: &'long mut [u8]) -> Self {
Here new takes a &'long mut [u8] where "'long outlives 'b" -- 'long: 'b. Other ways to think about this are that 'long is valid wherever 'b is (at least), and if something forces 'b to be valid, 'long is forced to be valid too. Unlike before, 'long is not independent of 'b.
This version works because the &'long mut [u8] can coerce to &'b mut [u8]. The rules around what coercions are possible are called variance. Or here's my brief introduction.
But you generally wouldn't actually write the function this way, when equality is sufficient. The consumer of this API isn't going to want the borrow they pass in to be longer than necessary -- longer than the return value is used. And that's all that this more general version does compared to the version that uses equality.
Finally I'll note that Self hides a lifetime in the function signature here.
impl<'b> FooBuffer<'b> {
pub fn new(buf: &'b mut [u8]) -> FooBuffer<'b> /* <-- Self */ {
I have another section where I go into what signatures like that mean. (Signatures where lifetimes flow from the inputs to the output type.)