Lifetimes on traits as return values

575 Views Asked by At

I would like to hide the actual implementation that is returned from the create function by returning an impl trait as in create_trait(). How could this be done?

trait Names<'a> {
    fn names(&'a self) -> &Vec<&'a str>;
}

struct NamesImpl<'b> {
    names: Vec<&'b str>,
}

impl<'c> Names<'c> for NamesImpl<'c> {
    fn names(&'c self) -> &Vec<&'c str> {
        &self.names
    }
}

fn create_impl<'i>() -> NamesImpl<'i> {
    NamesImpl {
        names: vec!["Hello", "world"],
    }
}

#[allow(dead_code)]
fn create_trait<'t>() -> impl Names<'t> {
    NamesImpl {
        names: vec!["Hello", "world"],
    }
}

fn main() {
    let names_impl = create_impl();
    println!("Names: {:?}", names_impl.names());

    //This doesn't compile, see error below
    let names_trait = create_trait();
    println!("Names: {:?}", names_trait.names());
}

I can't wrap my head around the following compile error. Why does it work with create_impl() but not with create_trait()?

error[E0597]: `names_trait` does not live long enough
  --> src/main.rs:34:29
   |
34 |     println!("Names: {:?}", names_trait.names());
   |                             ^^^^^^^^^^^ borrowed value does not live long enough
35 | }
   | -
   | |
   | `names_trait` dropped here while still borrowed
   | borrow might be used here, when `names_trait` is dropped and runs the destructor for type `impl Names<'_>`
3

There are 3 best solutions below

1
On

A general rule of thumb when it comes to using lifetimes in Rust is that if you don't know what you're doing, let the compiler infer the correct lifetimes for you. Once you get more experience, you'll learn when you actually need to use explicit lifetime annotations, but your example is not one of those situations. I was able make it compile by removing all the unnecessary lifetime annotations:

trait Names {
    fn names(&self) -> &Vec<&str>;
}

struct NamesImpl<'a> {
    names: Vec<&'a str>,
}

impl Names for NamesImpl<'_> {
    fn names(&self) -> &Vec<&str> {
        &self.names
    }
}

fn create_impl() -> NamesImpl<'static> {
    NamesImpl { names: vec!["Hello", "world"] }
}

fn create_trait() -> impl Names {
    NamesImpl { names: vec!["Hello", "world"] }
}

fn main() {
    let names_impl = create_impl();
    println!("Names: {:?}", names_impl.names());

    // compiles
    let names_trait = create_trait();
    println!("Names: {:?}", names_trait.names());
}

playground

To determine which lifetime annotations were unnecessary, I started by removing all of them and then only added lifetime annotations back to the areas where the compiler specifically asked me to.

0
On

This section in Common Rust Lifetime Misconceptions (Thank you pretzelhammer!) was an eye opener to me. It made mee see that in this construction

trait Names<'a> {
    fn names(&'a self) -> &Vec<&'a str>;
}

calling names() will borrow the struct for the rest of its lifetime.

Correcting this in the original code with explicit lifetime annotations is possible, but the result is far from beautiful. I just tried this in an attempt to better understand the 'why'.

trait Names<'a : 'f, 'f> {
    fn names(&self) -> &Vec<&'f str>;
}

struct NamesImpl<'b> {
    names: Vec<&'b str>,
}

impl <'c : 'f, 'f> Names<'c, 'f> for NamesImpl<'c> {
    fn names(&self) -> &Vec<&'f str> {
        &self.names
    }
}

fn create_impl<'i : 'f, 'f>() -> NamesImpl<'i> {
    NamesImpl{names: vec!["Hello", "world"]}
}

#[allow(dead_code)]
fn create_trait<'t : 'f, 'f>() -> impl Names<'t, 'f> {
    NamesImpl{names: vec!["Hello", "world"]}
}

fn main() {
    let names_impl = create_impl();
    println!("Names: {:?}", names_impl.names());

    //This compiles
    let names_trait = create_trait();
    println!("Names: {:?}", names_trait.names());
}

Conclusion: Follow the advice given by Shepmaster

0
On

I think this problem relates to a drop glue issue when you try to return an opaque type (here impl Names<'t>), and rust compiler must decide for what that lifetime param actrually is while being used given the signature.

Here is another similar problem with GAT, which is also opaque type in the context of that generic test function. (Variance woes Pt.1 - The Adventure of Creating a Wayland Compositor Toolbox in Rust, and the precise explanation NLL vs dropglue? Lifetime parameters on traits - help - The Rust Programming Language Forum)

Also notice the subtle difference on the fact that Trait<'a> is invariant of lifetime 'a, yet the struct is covariant.

For the op's question specifically, it seems while being a fully opaque type, the 't lifetime param from returned impl Names<'t> must outlive the whole existence of the return value until it gets dropped. However, the names method claim to borrow self for that same period of time, which we can not provided.

    //This doesn't compile, see error below
    let names_trait = create_trait();
    println!("Names: {:?}", names_trait.names());

    --> projects/temp/src/main.rs:34:29
    |
 33 |     let names_trait = create_trait();
    |         ----------- binding `names_trait` declared here
 34 |     println!("Names: {:?}", names_trait.names());
    |                             ^^^^^^^^^^^ borrowed value does not live long enough
 ...
 37 | }
    | -
    | | // EDITED: drop(names_trait)
    | `names_trait` dropped here while still borrowed
    |   // EDITED: <---- 't ended here, for which in names_trait.names() the names_trait should be borrowed
    | borrow might be used here, when `names_trait` is dropped and runs the destructor for type `impl Names<'_>`

I myself is still doubted on this problem since I haven't found any related documentations until I searched and get up to this topic. (Trying to understand that similar GAT drop glue problem). In case anything I've missed or simply got wrong, corrections and materials are welcomed.

But one thing is clear they are totally right, just get rid of that useless lifetime parameter of trait Names<'a>, because 'a only participates in names method's contract on its own, it is redundant to bound it with implementor's lifetime param or trait's lifetime param.

Or at least, having this if you would like to keep it.

trait Names<'a> {
    fn names<'b>(&'b self) -> &Vec<&'b str>;
    // 'a for other use
}
struct NamesImpl<'b> {
    names: Vec<&'b str>,
}
impl<'a, 'c> Names<'a> for NamesImpl<'c> {
    // 'c: 'b implicitly
    fn names<'b>(&'b self) -> &Vec<&'b str> {
        &self.names
    }
}

fn create_trait<'t>() -> impl Names<'t> {
    NamesImpl {
        names: vec!["Hello", "world"],
    }
}

fn main() {
    let names_trait = create_trait();
    println!("Names: {:?}", names_trait.names());
}