© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
C. MilanesiBeginning Rusthttps://doi.org/10.1007/978-1-4842-7208-4_24

24. More About Lifetimes

Carlo Milanesi1  
(1)
Bergamo, Italy
 
In this chapter, you will learn:
  • How to avoid having to write lifetime specifiers for some kinds of free functions and methods, because such specifiers can be inferred

  • Why lifetime specifiers are also needed for structs, tuple-structs, and enums containing references

  • How to write lifetime specifiers for structs, tuple-structs, and enums

Lifetime Elision

In the previous chapter we saw that every function signature must specify, for each returned reference, whether that reference has a static lifetime or otherwise to which function arguments its lifetime is associated.

This required annotation may be a nuisance, and sometimes it can be avoided. Let’s see an example that can be simplified:
trait Tr {
    fn f<'a>(x: &'a u8, b: bool) -> &'a u8;
}
In this code there is only one reference among the types in the function argument list, so there were only two possible lifetime annotations: let the return value type have a 'static lifetime specifier ; or do as in the preceding code (i.e.. let the return value type have the same lifetime specifier as the only reference in the argument list). For such a case of only one possible nonstatic lifetime specifier, the Rust language has introduced the simplification that, if the signature contains no lifetime specifier, the only possible one is assumed by default. Therefore, the preceding code is equivalent to the following valid code:
trait Tr {
    fn f(x: &u8, b: bool) -> &u8;
}
Even the following declaration is valid:
trait Tr {
    fn f(b: bool, x: (u32, &u8)) -> &u8;
}

This is because in the arguments there is just one reference, so it must be the one whose referred object is borrowed by the return value.

The following code is also valid:
trait Tr {
    fn f(x: &u8) -> (&u8, &f64, bool, &Vec<String>);
}

In this case there are several references in the return value, but still only one reference in the arguments.

You can also omit the lifetime specifier for some returned references only, and specify it for others:
trait Tr {
    fn f<'a>(x: &'a u8) -> (&u8, &'a f64, bool, &'static Vec<String>);
}

Here, the return value contains three references: the first one has unspecified lifetime, the second one has the 'a lifetime, and the third one (that is the fourth field of the tuple), has the 'static lifetime. However, there is still only one reference in the arguments, so the first returned reference has an implied 'a lifetime.

Such allowed omission of the lifetime specifier is named lifetime elision . To simplify the syntax, the lifetime specifier can be elided when there is only one possible nonstatic value, and that happens when there is exactly one reference among the function arguments.

Lifetime Elision with Object-Oriented Programming

There are other cases where a lifetime elision can be applied. Consider this code:
trait Tr {
    fn f(&self, y: &u8)
    -> (&u8, &f64, bool, &Vec<String>);
}
Here, the f function has two references in its arguments, so the previous rule does not apply. However, when a method returns some references, in most cases such references borrow the current object, which is referred to by &self. So, to simplify the syntax, the previous code is considered equivalent to the following one:
trait Tr {
    fn f<'a>(&'a self, y: &u8)
    -> (&'a u8, &'a f64, bool, &'a Vec<String>);
}
Here is a table of dependencies between the references that appear in this signature.

Reference Contained in the Return Value

Borrowed Argument

First field, having type &u8

&self

Second field, having type &f64

&self

Fourth field, having type &Vec<String>

&self

Yet, you can override such behavior for selected references. In case, say, you meant that the second returned reference had a lifetime associated to the y argument, you had to write:
trait Tr {
    fn f<'a>(&self, y: &'a u8)
    -> (&u8, &'a f64, bool, &Vec<String>);
}

Here, the object referred to by the second field of the returned tuple must live no longer than the object referred to by y, while the objects referred to by the first and fourth fields must live no longer than the object referred to by &self.

Here is a table of dependencies between the references that appear in the last signature.

Reference Contained in the Return Value

Borrowed Argument

First field, having type &u8

&self

Second field, having type &f64

y: &u8

Fourth field, having type &Vec<String>

&self

Of course, the same rule also applies also for a &mut self argument .

The Need for Lifetime Specifiers in Structs Declarations

In the previous chapter, we saw that this is valid:
let x: i32 = 12;
let y: &i32 = &x;
print!("{}", *y);

because, although y holds a reference to x, it lives less than x.

Instead, this is illegal:
let y: &i32;
{
    let x: i32 = 12;
    y = &x;
}
print!("{}", *y);

because y holds a reference to x, but it lives longer than x.

We also saw that function signatures have to be suitably annotated to perform the lifetime check of borrowings, considering only one function body at a time.

A similar issue occurs when a struct contains some references.

This code appears to be legal (but it isn’t; it emits the error message: missing lifetime specifier):
struct S {
    b: bool,
    ri: & i32,
}
let x: i32 = 12;
let y: S = S { b: true, ri: &x };
print!("{} {}", y.b, *y.ri);
while this one is clearly illegal:
struct S {
    b: bool,
    ri: & i32,
}
let y: S;
{
    let x: i32 = 12;
    y = S { b: true, ri: &x };
}
print!("{} {}", y.b, *y.ri);

The latter code is illegal because y, through its ri field, borrows x, but it lives longer than x.

This case was quite simple, but a more realistic program (explicitly containing the main function) could be:
// In some library code:
struct S {
    b: bool,
    ri: & i32,
}
fn create_s(ri: &i32) -> S {
    S { b: true, ri: ri }
}
// In application code:
fn main() {
    let y: S;
    {
        let x: i32 = 12;
        y = create_s(&x);
    }
    print!("{} {}", y.b, *y.ri);
}

This application code is invalid, because, by invoking create_s, a reference to x gets stored inside the y object, and so y borrows x, but y lives longer than x.

But how can the application programmer know that the create_s function stores into the returned object the reference it gets as an argument, if not by watching the function body? With this code, it is not possible, as shown by the following valid program, which has the same application code of the previous program but does not make y borrow x:
// In some library code:
struct S { b: bool, ri: &'static i32 }
fn create_s(ri: &i32) -> S {
    static ZERO: i32 = 0;
    static ONE: i32 = 1;
    S {
        b: true,
        ri: if *ri > 0 { &ONE } else { &ZERO },
    }
}
// In application code:
fn main() {
    let y: S;
    {
        let x: i32 = 12;
        y = create_s(&x);
    }
    print!("{} {}", y.b, *y.ri);
}

This program can be compiled, and it will print: true 1.

Notice that, in the declaration of the S struct, the ri field is annotated by the 'static lifetime. In this code, the create_s function uses the ri argument just to decide how to initialize the ri field of the structure to create and return. The value of such argument is not stored in the structure. In any case, the ri field will surely contain a reference to a static value, which can be ZERO or ONE, and such value will never be destroyed.

This create_s function has the same signature as that of the previous example; but the previous example was invalid, as the argument was stored in a field of the struct, while this example is valid, as the argument is discarded after having been used.

So, without lifetime specifiers, the application programmer would be forced to read the body of the create_s library function to know if that function stores the reference it gets as an argument into the returned object or not. And this is bad.

Therefore, there is a need for further lifetime annotations, to allow both the application programmer and the compiler to focus on one function at a time, avoiding having to analyze the body of the create_s function to discover if the lifetimes of the objects used in the main function are correct.

So, even structs, similarly to functions, must explicitly specify the lifetimes of every reference contained in their fields.

This explains why even the former, apparently valid snippet, in fact generates the missing lifetime specifier compilation error.

Possible Lifetime Specifiers for Structs

Actually, for the lifetime of a reference field of a struct, the Rust compiler allows only two possibilities:
  • Such field is allowed to refer only to static objects.

  • Such field is allowed to refer to static objects or to objects that, albeit nonstatic, preexist the whole struct object and live longer than it.

The first case is just the one considered by the last example program. In it there was the line:
struct S { b: bool, ri: &'static i32 }

Such a struct actually contains a reference, but it is a static reference that cannot be assigned the value of any borrowed reference. So, there is never a lifetime issue in such a case, provided only static references are assigned to the ri field.

Instead, applying the second case of lifetime specification, the following valid program can be obtained:
// In some library code:
struct S<'a> { b: bool, ri: &'a i32 }
fn create_s<'b>(ri: &'b i32) -> S<'b> {
    S { b: true, ri: ri }
}
// In application code:
fn main() {
    let x: i32 = 12;
    let y: S;
    y = create_s(&x);
    print!("{} {}", y.b, *y.ri);
}
It will print: true 12 . Here, the x variable is borrowed in a more persistent way by the create_s function . Indeed, it is stored in the ri field of the returned struct-object; and such object is used to initialize the y variable in the main function . Therefore, the y variable must live less than the x variable; and it does so. If the body of the main function is replaced by the following code:
    let y: S;
    {
        let x: i32 = 12;
        y = create_s(&x);
    }
    print!("{} {}", y.b, *y.ri);

the usual `x` does not live long enough error would appear.

To see that x could be stored inside the struct, it is not required to examine the body of the create_s function or the field list of the S struct; it is enough to examine the signature of the create_s function, and the signature of S, which is the portion of its declaration before the open brace.

By examining the signature of the create_s function, it appears that it gets a reference as argument; a value of S type is returned; and that argument and return value have the same lifetime specifier, 'b. That means that the returned struct must live less than the borrowed i32 object.

By examining the signature of the S struct, it appears that it is parameterized by a lifetime specifier, and that means that some of its fields are nonstatic references.

So, we found that the create_s function gets a reference as an argument and returns an object parameterized by the same lifetime specifier. This implies that such object could borrow the object referenced by the argument, by storing into itself that object.

The compiler must separately check the consistency of the struct declaration and the consistency of the functions using that struct type. The clause struct S<'a> means that S borrows some objects, and the clause ri: &'a i32 inside the struct body means that the ri field is a reference that borrows an object.

Therefore, each reference field in a struct can have only two legal syntaxes: field: &'static type or field: &'lifetime type, where lifetime is also a parameter of the struct itself. If there are no reference fields or only static reference fields, the struct can have no lifetime parameters.

So, there are several possible syntax errors caught by the compiler.
struct _S1 { _f: &i32 }
struct _S2<'a> { _f: &i32 }
struct _S3 { _f: &'a i32 }
struct _S4<'a> { _f: &'static i32 }
struct _S5 { _f: &'static i32 }
struct _S6<'a> { _f: &'a i32 }

The first four statements are illegal. The declarations of _S1 and _S2 are illegal because the _f field is a reference field with no lifetime specifier. The declaration of _S3 is illegal because the 'a lifetime specifier is not declared as a parameter of the struct; and the declaration of _S4 is illegal because the parameter 'a is never used inside the body of the struct.

However, the last two struct declarations are valid. _S5 contains a reference to static objects, while _S6 contains a reference to an object that anyway must live longer than the struct itself.

Lifetime Specifiers for Tuple-Structs and Enums

We saw that when defining a struct type containing references, lifetime specifiers are required. But tuple-structs and enums also are types that may contain references, so also for them, lifetime specifiers are required for any contained reference. Here are examples of them:
struct TS<'a>(&'a u8);
enum E<'a, 'b> {
    _A(&'a u8),
    _B,
    _C(bool, &'b f64, char),
    _D(&'static str),
}
let byte = 34;
let _ts = TS(&byte);
let _e = E::_A(&byte);

This code is valid, but if any lifetime specifier is removed, the usual missing lifetime specifier error is generated.

By the way, notice the definition of the E::_D field. That is a reference to a static string slice. But we’ve seen such things since the beginning of this book; they are the string literals.

Lifetime Specifiers for Mutable References

To make things simpler, we never mixed lifetime specifiers with mutable references. Actually, it is allowed, albeit it is quite unusual:
fn f<'a>(b: &'a mut u8) -> &'a u8 {
    *b += 1;
    b
}
let mut byte = 12u8;
let byte_ref = f(&mut byte);
print!("{}", *byte_ref);

It will print: 13. A reference to the byte is passed to f, which increments it and then returns back a reference to it. It is unusual, because when a mutable argument is passed to a function, usually there is no need to return a reference that borrows it. It is enough to reuse the passed reference.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.184.114