What is the difference between a generic lifetime in the impl vs. in an associated function?

Given these two snippets:

pub struct Foo<'a> {
  data: &'a str
}

impl<'a> Foo<'a> {
  pub fn new<'b>(data: &'b str) -> Foo<'b> {
    Foo { data }
  }
}
pub struct Foo<'a> {
  data: &'a str
}

impl<'a> Foo<'a> {
  pub fn new(data: &'a str) -> Foo<'a> {
    Foo { data }
  }
}

is there any concrete difference between them? What is the most idiomatic form?

1 Like

The second snippet is better style, since the name new implies a constructor, and a constructor should generally be defined like this:

impl<'a> Foo<'a> {
    pub fn new(data: &'a str) -> Self { /* ... */ }
}

that is, where the implementing type and the returned type are the same. (Inside an impl block, Self is a type alias for the implementing type.)

Practically there's not a lot of difference between the two signatures in this simple case, since you'd usually call the function as just Foo::new() either way and let type inference take care of determining the lifetime on the returned value.

3 Likes

The weird thing about this one is that the 'a is unconstrained when you just call Foo::new. It's pretty weird to call <Foo<'short>>::new(...) and get a Foo<'long> back.

If you switch the lifetime generics for type generics you'll get a more useful message:

pub struct Foo<T> {
  data: T
}

impl<T> Foo<T> {
  pub fn new<U>(data: U) -> Foo<U> {
    Foo { data }
  }
}

fn main() {
    Foo::new(123_i32);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fd6bcb61069438f2920ac97ae57dacab

error[E0282]: type annotations needed
  --> src/main.rs:12:5
   |
12 |     Foo::new(123_i32);
   |     ^^^^^^^^ cannot infer type for type parameter `T`

The same weirdness happens for the lifetime generics, you just don't see it because the compiler shrugs and say "meh, 'static I guess, then".

2 Likes

Thanks—originally I was using

impl Foo<'_> {
    pub fn new(data: &str) -> Foo { /* ... */ }
}

for constructors, but then I found out about Self and when I changed the return type to Self, lifetime elision failed and I had to explore the difference between the snippets I sent above. Actually this is a good follow-up question, with this snippet, Rust infers the lifetimes like the latter snippet of my original question, so wouldn't it make sense for the first snippet to be more idiomatic as it is consistent with Rust's lifetime elision?

Maybe I'm misreading, but that sounds like a contradiction to me: this snippet is inferred like the former snippet of your OP. Every function input parameter with an elided lifetime gets its own fresh lifetime binding.

But that does make the first/former version consistent with this maximally elided version. (Which is why I'm confused/read a contradiction.) However the elision rules were chosen to cover the most common cases -- that doesn't mean a signature with more elision is always more idiomatic.

See also the meaning of '_ in an impl header (vs. function signature):

If the type's lifetime is not relevant, you can leave it off using '_ .

(It's relevant to an associated constructor function.)

2 Likes

Worth noting that for the case of lifetime parameters (≠ type parameters), the one with the two distinct lifetime parameters will actually be more flexible without really that much of a hurdle. That is, given:

fn takes_constructor (
    new_foo: impl for<'local> FnOnce(&'local str) -> Foo<'local>,
)
{
    let local = String::from("local");
    let foo = new_foo(&local);
    foo.stuff();
}

then one can do takes_constructor(Foo::new) with the former snippet ('a and 'b), but not the latter (-> Self constructor). That being said, in both cases takes_constructor(|s| Foo::new(s)) shall work, obviously.

2 Likes

Sorry! I meant the former snippet not the latter, just a typo :sweat_smile: Thank you for the clarification on anonymous lifetimes, too.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.