Lifetime annotations on Struct "constructor"

I'm trying to understand if are there any implications in using one or the other approaches for lifetime annotations in the following case:

// Some fully-owned data container
struct DataSource {
...
}

struct Measurement<'ds> {
   data_source: &'ds DataSource,
   ...
}

impl<'ds> Measurement<'ds> {
  // Approach 1
  fn new(data_source: &'ds DataSource) -> Measurement<'ds> {
  Measurement {
     ds,
  }

  // Approach 2
  fn new<'a>(data_source: &'a DataSource) -> Measurement<'a> {
  Measurement {
     ds,
  }
}

I tried to reason about the implications of the two approaches, but I have a gut feeling that in this case, since we're creating a new instance of Measurement, it shouldn't really make any difference if the data_source argument's lifetime is tied to the 'ds lifetime of the impl block or not, since we're in both cases "bringing into existence" something new that will be tied to the lifetime of such data_source argument.
In other words, there's no pre-existing instance of Measurements whose lifetime might impose different restrictions / conditions.
But I don't know if this is a correct line-of-thought and I would like in any case to be able to talk about this with more strict / precise terminology.

More generally, I always struggle to visualize what the 'a in impl<'a> SomeStruct<'a> implies when relating to method / associated functions arguments' lifetimes, so any help in that department would also be greatly appreciated...

Thanks.

In the second case you introduce another lifetime parameter for no apparent reason.
Btw. for the lazy:

// Approach 3
impl Measurement<'_> {
    fn new(data_source: &DataSource) -> Measurement<'_> {
        Measurement { data_source }
    }
}
3 Likes

At least for generic type parameters, this method can result in needing extra annotations. Lifetimes are a bit better at being inferred, but they might run into the same kind of issue:

struct Newtype<T> {
   val: T,
}

impl<T> Newtype<T> {
    fn new<U>(val: U) -> Newtype<U> {
        Newtype { val }
    }
}

fn main() {
    let _ = Newtype::new(42u32);
}

error[E0282]: type annotations needed
  --> src/main.rs:12:13
   |
12 |     let _ = Newtype::new(42u32);
   |             ^^^^^^^^^^^^ cannot infer type of the type parameter `T` declared on the struct `Newtype`
   |
help: consider specifying the generic argument
   |
12 |     let _ = Newtype::<T>::new(42u32);
   |                    +++++
1 Like

Here's a similar question from another user with several useful answers: What is the difference between a generic lifetime in the impl vs. in an associated function? - #6 by Yandros

2 Likes

Oh, my bad!
I wasn't able to find this while searching for similar questions.
That pretty much answers my doubts, I now have a cleaner idea and, inspired by the playground mentioned in this comment I wrote down a snippet to summarise more or less everything:

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

impl<'a> Foo<'a> {
    /***** CONSTRUCTORS WITH ARGUMENT'S LIFETIME INDEPENDENT OF IMPLEMENTATION *****/

    // 1) lifetime parameter is explicitly specified
    pub fn new_independent_lifetime<'b>(data: &'b str) -> Foo<'b> {
        Foo { data }
    }

    // 2) elided lifetime, but still shown on the return type using the wildcard lifetime
    // see: https://rust-lang.github.io/rfcs/2115-argument-lifetimes.html#the-wildcard-lifetime
    // lifetime of return type is deduced to be the same as the lifetime of the argument
    pub fn new_independent_lifetime_elided(data: &str) -> Foo<'_> {
        Foo { data }
    }

    // 3) maximally elided lifetime, not even shown on the return type with the wildcard lifetime
    // lifetime of return type is deduced to be the same as the lifetime of the argument
    pub fn new_independent_lifetime_maximally_elided(data: &str) -> Foo {
        Foo { data }
    }

    /***** CONSTRUCTORS WITH ARGUMENT'S LIFETIME TIED TO IMPLEMENTATION *****/

    // 1) lifetime on the return type is explicitly specified to be the same as the lifetime of the argument
    pub fn new_tied_lifetime(data: &'a str) -> Foo<'a> {
        Foo { data }
    }

    // 2) lifetime on the return type expressed using the wildcard lifetime and inferred from the argument
    pub fn new_tied_lifetime_wildcard(data: &'a str) -> Foo<'_> {
        Foo { data }
    }

    // 3) lifetime on the return type elided and inferred from the argument
    pub fn new_tied_lifetime_elided(data: &'a str) -> Foo {
        Foo { data }
    }

    // 4) idiomatic, and the Self alias is the same as explicitly specifying Foo<'a>
    pub fn new_tied_lifetime_idiomatic(data: &'a str) -> Self {
        Self { data }
    }
}

fn f<'short>(s: &'short str) {
    // lifetime independent of implementation
    let _: Foo<'short> = Foo::<'static>::new_independent_lifetime(s);
    let _: Foo<'static> = Foo::<'short>::new_independent_lifetime("");

    let _: Foo<'short> = Foo::<'static>::new_independent_lifetime_elided(s);
    let _: Foo<'static> = Foo::<'short>::new_independent_lifetime_elided("");

    let _: Foo<'short> = Foo::<'static>::new_independent_lifetime_maximally_elided(s);
    let _: Foo<'static> = Foo::<'short>::new_independent_lifetime_maximally_elided("");

    // lifeime tied to implementation
    // NONE OF THE FOLLWING COMPILES
    //let _: Foo<'short> = Foo::<'static>::new_tied_lifetime(s);
    //let _: Foo<'static> = Foo::<'short>::new_tied_lifetime("");

    //let _: Foo<'short> = Foo::<'static>::new_tied_lifetime_wildcard(s);
    //let _: Foo<'static> = Foo::<'short>::new_tied_lifetime_wildcard("");

    //let _: Foo<'short> = Foo::<'static>::new_tied_lifetime_elided(s);
    //let _: Foo<'static> = Foo::<'short>::new_tied_lifetime_elided("");

    //let _: Foo<'short> = Foo::<'static>::new_tied_lifetime_idiomatic(s);
    //let _: Foo<'static> = Foo::<'short>::new_tied_lifetime_idiomatic("");
}
1 Like

Thanks! I have added it to my knowledge base about lifetimes :smiley: .

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.