Advanced Lifetimes - The Rust Programming Language (2023)

Advanced Lifetimes

Back in Chapter 10 in the “Validating References with Lifetimes” section, welearned how to annotate references with lifetime parameters to tell Rust howlifetimes of different references relate. We saw how every reference has alifetime but, most of the time, Rust will let you elide lifetimes. Here we’lllook at three advanced features of lifetimes that we haven’t covered yet:

  • Lifetime subtyping, a way to ensure that one lifetime outlives anotherlifetime
  • Lifetime bounds, to specify a lifetime for a reference to a generic type
  • Trait object lifetimes, how they’re inferred, and when they need to bespecified
Lifetime Subtyping Ensures One Lifetime Outlives Another

Lifetime subtyping is a way to specify that one lifetime should outlive anotherlifetime. To explore lifetime subtyping, imagine we want to write a parser.We’ll have a structure called Context that holds a reference to the stringwe’re parsing. We’ll write a parser that will parse this string and returnsuccess or failure. The parser will need to borrow the context to do theparsing. Implementing this would look like the code in Listing 19-12, exceptthis code doesn’t have the required lifetime annotations so it won’t compile:

Filename: src/lib.rs

struct Context(&str);struct Parser { context: &Context,}impl Parser { fn parse(&self) -> Result<(), &str> { Err(&self.context.0[1..]) }}

Listing 19-12: Defining a parser without lifetimeannotations

Compiling the code results in errors saying that Rust expected lifetimeparameters on the string slice in Context and the reference to a Context inParser.

For simplicity’s sake, our parse function returns a Result<(), &str>. Thatis, it will do nothing on success, and on failure will return the part of thestring slice that didn’t parse correctly. A real implementation would have moreerror information than that, and would actually return something when parsingsucceeds, but we’ll leave those off because they aren’t relevant to thelifetimes part of this example.

To keep this code simple, we’re not going to actually write any parsing logic.It’s very likely that somewhere in parsing logic we’d handle invalid input byreturning an error that references the part of the input that’s invalid, andthis reference is what makes the code example interesting with regards tolifetimes. So we’re going to pretend that the logic of our parser is that theinput is invalid after the first byte. Note that this code may panic if thefirst byte is not on a valid character boundary; again, we’re simplifying theexample in order to concentrate on the lifetimes involved.

To get this code compiling, we need to fill in the lifetime parameters for thestring slice in Context and the reference to the Context in Parser. Themost straightforward way to do this is to use the same lifetime everywhere, asshown in Listing 19-13:

Filename: src/lib.rs

# #![allow(unused_variables)]#fn main() {struct Context<'a>(&'a str);struct Parser<'a> { context: &'a Context<'a>,}impl<'a> Parser<'a> { fn parse(&self) -> Result<(), &str> { Err(&self.context.0[1..]) }}#}

Listing 19-13: Annotating all references in Context andParser with the same lifetime parameter

This compiles fine, and tells Rust that a Parser holds a reference to aContext with lifetime 'a, and that Context holds a string slice that alsolives as long as the reference to the Context in Parser. Rust’s compilererror message said lifetime parameters were required for these references, andwe have now added lifetime parameters.

(Video) Rust Lifetimes Finally Explained!

Next, in Listing 19-14, let’s add a function that takes an instance ofContext, uses a Parser to parse that context, and returns what parsereturns. This won’t quite work:

Filename: src/lib.rs

fn parse_context(context: Context) -> Result<(), &str> { Parser { context: &context }.parse()}

Listing 19-14: An attempt to add a parse_contextfunction that takes a Context and uses a Parser

We get two quite verbose errors when we try to compile the code with theaddition of the parse_context function:

error[E0597]: borrowed value does not live long enough --> src/lib.rs:14:5 |14 | Parser { context: &context }.parse() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not live long enough15 | } | - temporary value only lives until here |note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 13:1... --> src/lib.rs:13:1 |13 | / fn parse_context(context: Context) -> Result<(), &str> {14 | | Parser { context: &context }.parse()15 | | } | |_^error[E0597]: `context` does not live long enough --> src/lib.rs:14:24 |14 | Parser { context: &context }.parse() | ^^^^^^^ does not live long enough15 | } | - borrowed value only lives until here |note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 13:1... --> src/lib.rs:13:1 |13 | / fn parse_context(context: Context) -> Result<(), &str> {14 | | Parser { context: &context }.parse()15 | | } | |_^

These errors are saying that both the Parser instance that’s created and thecontext parameter live only from when the Parser is created until the endof the parse_context function, but they both need to live for the entirelifetime of the function.

In other words, Parser and context need to outlive the entire functionand be valid before the function starts as well as after it ends in order forall the references in this code to always be valid. Both the Parser we’recreating and the context parameter go out of scope at the end of thefunction, though (because parse_context takes ownership of context).

To figure out why we’re getting these errors, let’s look at the definitions inListing 19-13 again, specifically the references in the signature of theparse method:

 fn parse(&self) -> Result<(), &str> {

Remember the elision rules? If we annotate the lifetimes of the referencesrather than eliding, the signature would be:

 fn parse<'a>(&'a self) -> Result<(), &'a str> {

That is, the error part of the return value of parse has a lifetime that istied to the lifetime of the Parser instance (that of &self in the parsemethod signature). That makes sense: the returned string slice references thestring slice in the Context instance held by the Parser, and the definitionof the Parser struct specifies that the lifetime of the reference toContext and the lifetime of the string slice that Context holds should bethe same.

The problem is that the parse_context function returns the value returnedfrom parse, so the lifetime of the return value of parse_context is tied tothe lifetime of the Parser as well. But the Parser instance created in theparse_context function won’t live past the end of the function (it’stemporary), and context will go out of scope at the end of the function(parse_context takes ownership of it).

Rust thinks we’re trying to return a reference to a value that goes out ofscope at the end of the function, because we annotated all the lifetimes withthe same lifetime parameter. That told Rust the lifetime of the string slicethat Context holds is the same as that of the lifetime of the reference toContext that Parser holds.

(Video) Rust: Generics, Traits, Lifetimes

The parse_context function can’t see that within the parse function, thestring slice returned will outlive both Context and Parser, and that thereference parse_context returns refers to the string slice, not to Contextor Parser.

By knowing what the implementation of parse does, we know that the onlyreason the return value of parse is tied to the Parser is because it’sreferencing the Parser’s Context, which is referencing the string slice, soit’s really the lifetime of the string slice that parse_context needs to careabout. We need a way to tell Rust that the string slice in Context and thereference to the Context in Parser have different lifetimes and that thereturn value of parse_context is tied to the lifetime of the string slice inContext.

First we’ll try giving Parser and Context different lifetime parameters asshown in Listing 19-15. We’ll use 's and 'c as lifetime parameter names tobe clear about which lifetime goes with the string slice in Context and whichgoes with the reference to Context in Parser. Note that this won’tcompletely fix the problem, but it’s a start and we’ll look at why this isn’tsufficient when we try to compile.

Filename: src/lib.rs

struct Context<'s>(&'s str);struct Parser<'c, 's> { context: &'c Context<'s>,}impl<'c, 's> Parser<'c, 's> { fn parse(&self) -> Result<(), &'s str> { Err(&self.context.0[1..]) }}fn parse_context(context: Context) -> Result<(), &str> { Parser { context: &context }.parse()}

Listing 19-15: Specifying different lifetime parametersfor the references to the string slice and to Context

We’ve annotated the lifetimes of the references in all the same places that weannotated them in Listing 19-13, but used different parameters depending onwhether the reference goes with the string slice or with Context. We’ve alsoadded an annotation to the string slice part of the return value of parse toindicate that it goes with the lifetime of the string slice in Context.

The following is the error we get now when we try to compile:

error[E0491]: in type `&'c Context<'s>`, reference has a longer lifetime than the data it references --> src/lib.rs:4:5 |4 | context: &'c Context<'s>, | ^^^^^^^^^^^^^^^^^^^^^^^^ |note: the pointer is valid for the lifetime 'c as defined on the struct at 3:1 --> src/lib.rs:3:1 |3 | / struct Parser<'c, 's> {4 | | context: &'c Context<'s>,5 | | } | |_^note: but the referenced data is only valid for the lifetime 's as defined on the struct at 3:1 --> src/lib.rs:3:1 |3 | / struct Parser<'c, 's> {4 | | context: &'c Context<'s>,5 | | } | |_^

Rust doesn’t know of any relationship between 'c and 's. In order to bevalid, the referenced data in Context with lifetime 's needs to beconstrained, to guarantee that it lives longer than the reference with lifetime'c. If 's is not longer than 'c, the reference to Context might not bevalid.

Which gets us to the point of this section: the Rust feature lifetimesubtyping is a way to specify that one lifetime parameter lives at least aslong as another one. In the angle brackets where we declare lifetimeparameters, we can declare a lifetime 'a as usual, and declare a lifetime'b that lives at least as long as 'a by declaring 'b with the syntax 'b: 'a.

In our definition of Parser, in order to say that 's (the lifetime of thestring slice) is guaranteed to live at least as long as 'c (the lifetime ofthe reference to Context), we change the lifetime declarations to look likethis:

Filename: src/lib.rs

(Video) Advanced Lifetimes and Generics in Rust

# #![allow(unused_variables)]#fn main() {# struct Context<'a>(&'a str);#struct Parser<'c, 's: 'c> { context: &'c Context<'s>,}#}

Now, the reference to Context in the Parser and the reference to the stringslice in the Context have different lifetimes, and we’ve ensured that thelifetime of the string slice is longer than the reference to the Context.

That was a very long-winded example, but as we mentioned at the start of thischapter, these features are pretty niche. You won’t often need this syntax, butit can come up in situations like this one, where you need to refer tosomething you have a reference to.

Lifetime Bounds on References to Generic Types

In the “Trait Bounds” section of Chapter 10, we discussed using trait bounds ongeneric types. We can also add lifetime parameters as constraints on generictypes, and these are called lifetime bounds. Lifetime bounds help Rust verifythat references in generic types won’t outlive the data they’re referencing.

For an example, consider a type that is a wrapper over references. Recall theRefCell<T> type from the “RefCell<T> and the Interior Mutability Pattern”section of Chapter 15: its borrow and borrow_mut methods return the typesRef and RefMut, respectively. These types are wrappers over references thatkeep track of the borrowing rules at runtime. The definition of the Refstruct is shown in Listing 19-16, without lifetime bounds for now:

Filename: src/lib.rs

struct Ref<'a, T>(&'a T);

Listing 19-16: Defining a struct to wrap a reference to ageneric type; without lifetime bounds to start

Without explicitly constraining the lifetime 'a in relation to the genericparameter T, Rust will error because it doesn’t know how long the generictype T will live:

error[E0309]: the parameter type `T` may not live long enough --> src/lib.rs:1:19 |1 | struct Ref<'a, T>(&'a T); | ^^^^^^ | = help: consider adding an explicit lifetime bound `T: 'a`...note: ...so that the reference type `&'a T` does not outlive the data it points at --> src/lib.rs:1:19 |1 | struct Ref<'a, T>(&'a T); | ^^^^^^

Because T can be any type, T could itself be a reference or a type thatholds one or more references, each of which could have their own lifetimes.Rust can’t be sure T will live as long as 'a.

Fortunately, that error gave us helpful advice on how to specify the lifetimebound in this case:

consider adding an explicit lifetime bound `T: 'a` so that the reference type`&'a T` does not outlive the data it points at

Listing 19-17 shows how to apply this advice by specifying the lifetime boundwhen we declare the generic type T.

# #![allow(unused_variables)]#fn main() {struct Ref<'a, T: 'a>(&'a T);#}

Listing 19-17: Adding lifetime bounds on T to specifythat any references in T live at least as long as 'a

(Video) The Rust Programming Language - Lifetime - Video 37

This code now compiles because the T: 'a syntax specifies that T can be anytype, but if it contains any references, the references must live at least aslong as 'a.

We could solve this in a different way, shown in the definition of aStaticRef struct in Listing 19-18, by adding the 'static lifetime bound onT. This means if T contains any references, they must have the 'staticlifetime:

# #![allow(unused_variables)]#fn main() {struct StaticRef<T: 'static>(&'static T);#}

Listing 19-18: Adding a 'static lifetime bound to Tto constrain T to types that have only 'static references or noreferences

Because 'static means the reference must live as long as the entire program,a type that contains no references meets the criteria of all references livingas long as the entire program (because there are no references). For the borrowchecker concerned about references living long enough, there’s no realdistinction between a type that has no references and a type that hasreferences that live forever; both of them are the same for the purpose ofdetermining whether or not a reference has a shorter lifetime than what itrefers to.

Inference of Trait Object Lifetimes

In Chapter 17 in the “Using Trait Objects that Allow for Values of DifferentTypes” section, we discussed trait objects, consisting of a trait behind areference, that allow us to use dynamic dispatch. We haven’t yet discussed whathappens if the type implementing the trait in the trait object has a lifetimeof its own. Consider Listing 19-19, where we have a trait Red and a structBall. Ball holds a reference (and thus has a lifetime parameter) and alsoimplements trait Red. We want to use an instance of Ball as the traitobject Box<Red>:

Filename: src/main.rs

trait Red { }struct Ball<'a> { diameter: &'a i32,}impl<'a> Red for Ball<'a> { }fn main() { let num = 5; let obj = Box::new(Ball { diameter: &num }) as Box<Red>;}

Listing 19-19: Using a type that has a lifetime parameterwith a trait object

This code compiles without any errors, even though we haven’t said anythingexplicit about the lifetimes involved in obj. This works because there arerules having to do with lifetimes and trait objects:

  • The default lifetime of a trait object is 'static.
  • With &'a Trait or &'a mut Trait, the default lifetime is 'a.
  • With a single T: 'a clause, the default lifetime is 'a.
  • With multiple T: 'a-like clauses, there is no default; we mustbe explicit.

When we must be explicit, we can add a lifetime bound on a trait object likeBox<Red> with the syntax Box<Red + 'a> or Box<Red + 'static>, dependingon what’s needed. Just as with the other bounds, this means that anyimplementor of the Red trait that has references inside must have thesame lifetime specified in the trait object bounds as those references.

Next, let’s take a look at some other advanced features dealing with traits!

FAQs

What are lifetimes explained in Rust? ›

What are lifetimes explained in Rust?

How do you use lifetime in Rust? ›

How do you use lifetime in Rust?

What is the default lifetime of a trait Rust? ›

What is the default lifetime of a trait Rust?

What is lifetime function signature Rust? ›

What is lifetime function signature Rust?

What is anonymous lifetime Rust? ›

What is anonymous lifetime Rust?

Videos

1. Crust of Rust: Lifetime Annotations
(Jon Gjengset)
2. Easy Rust 103: Anonymous lifetimes
(mithradates)
3. Introduction to Rust - Part 9: Lifetimes
(Rhymu's Videos)
4. Rust Intro to Generics, Traits, Lifetimes
(The Dev Method)
5. Rust: Lifetimes Part 1
(Crazcalm's Tech Stack)
6. Chapter 11 - Lifetimes -- Rust Crash Course 🦀
(Vandad Nahavandipoor)
Top Articles
Latest Posts
Article information

Author: Jeremiah Abshire

Last Updated: 15/06/2023

Views: 5729

Rating: 4.3 / 5 (54 voted)

Reviews: 85% of readers found this page helpful

Author information

Name: Jeremiah Abshire

Birthday: 1993-09-14

Address: Apt. 425 92748 Jannie Centers, Port Nikitaville, VT 82110

Phone: +8096210939894

Job: Lead Healthcare Manager

Hobby: Watching movies, Watching movies, Knapping, LARPing, Coffee roasting, Lacemaking, Gaming

Introduction: My name is Jeremiah Abshire, I am a outstanding, kind, clever, hilarious, curious, hilarious, outstanding person who loves writing and wants to share my knowledge and understanding with you.