Lifetime Specifiers in Rust

enigbe ochekliye
5 min readMar 14, 2022

--

Lifetimes, ownership, and borrowing are some of the fundamental concepts to grasp if I must write fluent rust. Lifetimes, in particular, ensure that functions, closures, structs, enums, and structs that own certain objects remain their owners after other variables have borrowed and/or returned them. It is “some stretch of your program for which a reference could be safe to use”.

They are “structures” we put in place to inform the compiler that objects borrowed remain, even after the borrowing object has been destroyed. We never really have to worry about lifetimes except when we create structs whose fields have borrowed data, or when we want to return borrowed data from a function.

I learned how to build a utility bitcoin library, in Python, in February, and set a goal to port the said library to Rust. In the course of doing so, I ran into a missing lifetime specifier error that questioned my knowledge of lifetimes.

This article is captures what I learned about lifetimes that helped me resolve my error. It presents a simple explanation of struct and function lifetimes.

Struct Lifetimes

Say, for example, that you have a structure that models a base dimension with two fields: length and breadth, as shown below

Referenced BaseDimension structure

If another structure, a Tank, references an instance of BaseDimension, then we know that the instance of the BaseDimension struct must exist before the Tank struct is created and must remain after the tank is destroyed. We would need to reference the BaseDimension instance.

Struct with reference field (no lifetime specification)

To define a lifetime for a structure, we use the following syntax:

Struct lifetime specifier

For our tank example, we define a lifetime 'base for the BaseDimension as shown below, without which you would encounter a missing lifetime specifier error.

Struct with reference field and lifetime specifier

You can run a test of the code below on the rust playground. Here is a walk-through:

  1. [Lines 2–5]: A BaseDimension struct is defined and has two fields: length and breadth
  2. [Line 8]: A Colour enum with three (3) variances R, G, and B is defined.
  3. [Lines 11–14]: A generic Tank structure with a lifetime of 'base is defined, having two fields: colour which is an enumeration of R, G, or B, and 'base which is a reference to a BaseDimension structure with a defined lifetime of 'base .
  4. [Lines 16–21]: Within main, a base instance and a tank instance, with reference to the base, are created. The println! macro references the tank instance after it is called. The base and tank instances remain alive. Note that base20x40 will be alive for however long tank20x40 is, and can only be safe for destruction after the tank instance has been dropped.
Listing 1: Struct lifetime specifier

Function Lifetimes

The second case to consider where lifetimes matter is with functions and returning borrowed values from them. The syntax is as shown below. Here we see the creation of two lifetimes 'lt_a and 'lt_b belonging to instances of TypeA and TypeB respectively. This function func returns a tuple of references to types TypeA and TypeB.

Definition of function lifetime specifier

An illustrative example to show the use of lifetimes in a function is shown in Listing 2.

Listing 2: Function lifetime specifier

A walk-through of the example is listed below:

  1. [Lines 2–6]: A City struct with three String fields: name , country , continent is defined.
  2. [Lines 9–12]: A Hospital struct with a lifetime of 'city which will reference a borrowed instance of City
  3. [Lines 14–16]: A function healthcare_centres with lifetime specifiers 'h and 'cityfor the parameters that are references to instances of &Hospital and City . Note how the struct lifetime is passed and the type of the return value: a tuple of borrowed objects.
  4. [Lines 18–34]: Within main, instances of City and Hospital are created, with their references passed as arguments to healthcare_centre . The println! macros show how objects referenced by the function and tracked by their lifetimes outlive the function that called them.

The Rust compiler knows to save referenced instances in such a manner that it will outlive the function call. With the defined lifetimes, Rust can assess the relationship between the arguments passed to the function and the values returned in a safe way. We can tell that the reference(s) of the returned value from the function points to the arguments passed, and no where else.

Note:

  • A special lifetime 'static is reserved by Rust for objects that must remain in memory until the program ends.
  • Rust infers lifetimes. They (lifetimes) are only needed in defining functions or types.

Conclusion

Lifetimes are ways the compiler keeps track of what objects can/must remain alive as they are borrowed by other objects. This is how I like to think about them. If you liked this article and would like to see where I am applying some of the new Rust knowledge I have gained over the past two weeks, please check out bitlib — my attempt to port a bitcoin utility library to Rust.

Looking forward to any feedback I can get.

References

  1. Blandy Jim, Orendorff, J., & Tindall, F, S, Leonora. (2021). Programming rust: Fast, safe systems development
    (2nd ed.) O’Reilly Media Inc.
  2. Rust Programming: The Complete Developer’s Guide by Jason Lennon

--

--

enigbe ochekliye
enigbe ochekliye

Written by enigbe ochekliye

Mechanical engineer. Software Developer.

Responses (3)