Understanding Rust's Trait Objects and Lifetime Annotations in Different Function Signatures

77 views Asked by At

I am studying the Traits and the Trait Object in Rust. In the Trait chapter, I solved the 6th exercise differently than the compiler suggestion. The following code defines two structs (Sheep and Cow) and a trait (Animal). Both Sheep and Cow implement the Animal trait by providing their own noise method implementations.

There are two functions, the random_animal_if and the random_animal_match that take an argument and return a reference to a dynamic trait object.

struct Sheep {}
struct Cow {}

trait Animal {
    fn noise(&self) -> String;
}

impl Animal for Sheep {
    fn noise(&self) -> String {
        "baaaaah!".to_string()
    }
}

impl Animal for Cow {
    fn noise(&self) -> String {
        "moooooo!".to_string()
    }
}

fn random_animal_if(random_number: f64) -> &'static dyn Animal {
    if random_number < 10.0 {
        &Sheep {}
    } else if random_number > 20.0 {
        &Cow {}
    } else {
        panic!()
    }
}

fn random_animal_match(random_string: &str) -> &dyn Animal {
    match random_string {
        "sheep" => &Sheep {},
        "cow" => &Cow {},
        _ => panic!(),
    }
}

fn main() {
    let animal = random_animal_if(21.0);
    println!("Randomly animal says {}", animal.noise());
    let animal = random_animal_match("sheep");
    println!("Randomly animal says {}", animal.noise());
}

Both function creates and returns either a Sheep or Cow object based on the input. One of them uses conditionals on a floating number input. The other uses pattern matching on a given string slice. The logic is identical, but if I omit the &'static lifetime specification at random_animal_if return type then the compiler throws this error:

error[E0106]: missing lifetime specifier

Interestingly, if the input parameter type is changed from f64 to &str then the static lifetime annotation can be removed. Why? What is the difference between the two types?

2

There are 2 answers

0
Peter Hall On BEST ANSWER

This is due to Rust's lifetime elision rules.

If you have a function that takes a reference as an argument and returns a reference, then the compiler infers that the output borrows from the input, which looks like this:

fn random_animal_match<'a>(random_str: &'a str) -> &'a dyn Animal {

In fact, the only way for a function to return a non-static reference is if the returned data is borrowed from an argument.

However, in your code, the body of random_animal_match does not borrow the return value from the argument. The compiler still infers the elided lifetimes as if that is the case but in fact the lifetime in the return type is always 'static. This means that the lifetime in your function's return type is overly restrictive. A caller of the function will get compiler errors if they try to use the returned &dyn Animal after the input &str is dropped, even though this shouldn't actually be a problem:

fn main() {
    let animal = {
        let sheep = String::from("sheep");
        random_animal_match(&sheep)
    }; // - `sheep` dropped here while still borrowed

    // `sheep` does not live long enough
    println!("Randomly animal says {}", animal.noise());
}

To maximise the flexibility of this function, you should make the lifetime in the return type 'static:

fn random_animal_match(random_str: &str) -> &'static dyn Animal {
0
Max Meijer On

&str is a reference and has a lifetime associated with it, whereas f64 doesn't. The f64 type is owned by this function: it is stored on the stack and will be deallocated when the function returns. The underlying string is owned by some other function and has only been borrowed.

The returned value is also a reference. This means it must have a lifetime. In some cases the lifetime does not need to be specified because it can be inferred. There is one basic rule for this:

If there is exactly one lifetime used in the parameters (elided or not), that lifetime is assigned to all elided output lifetimes.

In the f64 case, there is no lifetime parameter because it is not a reference. Hence, the rule does not apply. In the &str case there is a lifetime parameter that has been left out (elided). Hence, there is exactly one lifetime used in the parameters and so the rule applies and the return type gets the same lifetime. In the case of f64, the rule does not apply so no lifetime is assigned to the return type but that means that the function signature is not valid because the lifetime specifier is missing.

Now, this means that you could leave out the lifetime parameter in the case of &str, but it does change the semantics. The inferred lifetime would be the same as that of the string, meaning that whenever the string gets deallocated, the returned reference to the animal becomes invalid as well. This might be inconvenient if the intention is to keep the animal around for longer than the string. Therefore, it is better to specify the lifetime to be 'static in both cases.