1

In chapter 19.2 of The Rust Programming Language, the following example compiles without any error. I found out from issue #1834 that there is a new lifetime elision rule that implicitly makes 's longer than 'c.

Although I couldn't find a detailed explanation of this new elision rule, I guess that it is not more than just an implicit version of the longer, more explicit constraint: <'c, 's: 'c>. I think however my confusion is probably not about this new elision rule but of course I could be wrong about this.

My understanding is, that parse_context takes ownership of context as it has not been borrowed but actually moved to the function. That alone implies to me that the lifetime of context should match the lifetime of the function it is owned by regardless of the lifetimes and constraint we defined in Context, and Parser.

Based on those definitions, the part where context outlives the temporary Parser makes perfect sense to me (after all, we defined a longer lifetime), but the part where the &str reference is not dropped when context goes out of scope at the end of parse_context and I can still safely return it -- makes me puzzled.

What have I missed? How can the compiler reason about the lifetime of the returned &str?

UPDATED EXAMPLE

struct Context<'s>(&'s str);

struct Parser<'c, 's>
{
    context: &'c Context<'s>,
}

impl<'c, 's> Parser<'c, 's>
{
    fn parse(&self) -> Result<(), &'s str>
    {
        Err(self.context.0)
    }
}

fn parse_context(context: Context) -> Result<(), &str>
{
    Parser { context: &context }.parse()
}

fn main()
{
    let mut s = String::new();
    s += "Avail";
    s += "able?";
    if let Err(text) = parse_context(Context(&s))
    {
        println!("{}", text);
    }
}
Jeremy
  • 1
  • 77
  • 324
  • 346
Peter Varo
  • 10,078
  • 6
  • 42
  • 63

3 Answers3

2

You are not returning a reference to the owned value of the function. You are returning a copy of the reference passed in.

Your function is an elaborate version of the identity function:

fn parse_context(s: &str) -> &str {
    s
}

In your real code, you take a reference to a struct containing a string slice, then another reference to the string slice, but all of those references are thrown away.

For example, there's an unneeded reference in parse:

fn parse(&self) -> Result<(), &'s str> {
    Err( self.context.0)
    //  ^ no & needed
}

Additionally, if you enable some more lints, you'll be forced to add more lifetimes to your function signature, which might make things more clear:

#![deny(rust_2018_idioms)]

fn parse_context(context: Context<'_>) -> Result<(), &'_ str> {
    Parser { context: &context }.parse()
}

See also:

Although I couldn't find a detailed explanation of this new elision rule,

T: 'a inference in structs in the edition guide.

Peter Varo
  • 10,078
  • 6
  • 42
  • 63
Shepmaster
  • 274,917
  • 47
  • 731
  • 969
  • Let me get this straight then: in the identity function the copy of `s` (the type of which is a reference) isn't the copy of the reference itself but also entire object it is referencing (in our case the slice)? – Peter Varo Apr 24 '19 at 06:28
  • No, the copy of `s` is indeed the copy of the reference. However in your case it is a reference to the `'static` string `"Available?"` defined in your `main` function that will never be dropped since it's `'static`. – Jmb Apr 24 '19 at 07:08
  • @Jmb that's exactly what I thought earlier, but as you can see -- I updated the example -- even if I create a `String` on the heap, add some characters to it, and then passing to the `Context` (at which point I don't think that the underlying `&str` has `'static` anymore) the code still compiles and working without a problem.. Why? – Peter Varo Apr 24 '19 at 08:41
  • Just a thought: is this snippet valid, because the temporary `Context` created in the `main`'s scope will only be dropped at the end of the `if let`'s scope because that is taking ownership of the return value of `.parse()`? – Peter Varo Apr 24 '19 at 08:48
  • 2
    It's no longer `'static`, but it's still a reference to the string defined in `main`, so it's still valid for the duration of `main`. IOW, lifetime `'s` is inferred as "until the end of function `main`". – Jmb Apr 24 '19 at 08:50
  • @Jmb I think I understand now, posted an answer explaining my understanding of this. Do you think it is correct now? – Peter Varo Apr 24 '19 at 09:11
  • 2
    @PeterVaro it’s not “my” style, it’s the Rust community style as agreed upon via RFCs and enacted by rustfmt. I edit every Rust Q&A to provide a uniform experience for people looking for answers to problems without wanting to learn an arbitrary posters white space whims. The point of SO is to quickly read a question to see if it is relevant and the answer to gain information. Non-idiomatic style choices detract from that goal. – Shepmaster Apr 24 '19 at 10:30
  • 1
    @Shepmaster yeah, except neither the standard library, nor other _official_ code snippets are following the exact same rules as I found out so far, and `rustfmt` (thank god) is configurable and not as stupidly aggressive as `gofmt` or `prettier`.. – Peter Varo Apr 24 '19 at 12:54
1
struct Context<'s>(&'s str);

→ Values of type Context hold a string with some lifetime 's. This lifetime is implicitly at least as long as the lifetime of the context, but it may be longer.

struct Parser<'c, 's>
{
    context: &'c Context<'s>,
}

→ Values of type Parser hold a a reference to context with some lifetime 'c. This context holds a string with some other lifetime 's.

impl<'c, 's> Parser<'c, 's>
{
    fn parse(&self) -> Result<(), &'s str>
    {
        Err(self.context.0)
    }
}

→ Function parse returns a value with lifetime 's, ie. with the same lifetime as the string that is stored inside the context, which is not the same as the lifetime of the context itself.

fn parse_context(context: Context) -> Result<(), &str>
{
    Parser { context: &context }.parse()
}

→ I'm not sure exactly where this is specified, but obviously the compiler infers that the lifetime for the returned string is the same as the 's parameter used for the context. Note that even though the context itself is moved into parse_context, this only affects the context itself, not the string that it contains.

fn main()
{
    let mut s = String::new();

→ Create a new string valid until the end of main

    s += "Avail";
    s += "able?";
    if let Err(text) = parse_context(Context(&s))

→ Create a new context and move it into parse_context. It will automatically be dropped at the end of parse_context. It holds a reference to string s which is valid until the end of main and parse_context returns a string with the same lifetime as stext is valid until the end of main.

    {
        println!("{}", text);
    }
}

→ No problem: text is valid until the end of main.

Jeremy
  • 1
  • 77
  • 324
  • 346
Jmb
  • 9,119
  • 14
  • 36
  • It is a much more detailed explanation of the same conclusion that I summarised in my answer. This was exactly what I was looking for. Thanks! – Peter Varo Apr 24 '19 at 09:22
  • Please always use `rustfmt` to format your code according to the guidelines. You can find it under tools in the upper right corner of [the playground](https://play.rust-lang.org) – hellow Apr 24 '19 at 13:09
-1

Thank to the comments of Jmb and some bits of Shepmaster's answer it is indeed clear to me now that my confusion was about the RAII rules and the ownership.

That is, the string was created in the main scope, the anonymous Context instance is not taking ownership of the string it is only borrowing a reference, therefore even when the instance is dropped at the end of parse_context along with the borrowed reference, a reference copied to the Err object still exists and points to the existing string -- hence we are using the constrained lifetime variables and the compiler is able to reason about the lifetime of the internal string reference.

Peter Varo
  • 10,078
  • 6
  • 42
  • 63