Really basic: lifetimes in `match`


#1

I’m just getting acquainted with Rust’s borrowing. I realize this is incredibly basic, but can someone explain to me why this code is wrong (playpen)?

let a = Some("foo".to_string());
let p = match a {
    Some(s) => Path::new(&s),
    None => Path::new("bar"),
};
println!("{:?}", p);

Rust complains (shortened):

error: `s` does not live long enough
reference must be valid for the block suffix following statement 1 at 8:6...
...but borrowed value is only valid for the match at 5:12

My main question is: Why does s have to outlive the match? I only want to use it inside the match, to construct a new Path, so why do I seem to be required to make it live longer?

I would also love to hear how I’m doing this wrong. Is there a more idiomatic way to write this construct (invoking something on an optional String, with a default fallback)?

I also considered something like:

 Path::new(a.unwrap_or("bar".to_string()))

but my intuition said that it would be unnecessary to create a String from the &'static str just to throw it away. Perhaps that intuition is wrong.


#2

s is only local to that match arm, ie, this line of code:

Some(s) => Path::new(&s),

Why does s have to outlive the match?

Well, you said that the new Path was built off of &s. So if s goes out of scope, that’s a dangling pointer.

It’s actually easier to use some standard functions on Option:

let a = Some("foo".to_string());
let b = a.unwrap_or("bar".to_string());
let p = Path::new(&b);

println!("{:?}", p);

EDIT: Sigh, just saw that that at the bottom, my bad. Yes, that’s a slightly awkward cost of doing it this way. I think you might be able to get around it…


#3

a.as_ref().map(AsRef::as_ref).unwrap_or("bar"); should work for your case.

.as_ref() turns Option<String> into Option<&String>, then .map(AsRef::as_ref) turns Option<&String> into Option<&str>. With an Option<&str>, .unwrap_or() works correctly given a static string.

let a = Some("foo".to_string());
let b = a.as_ref().map(AsRef::as_ref).unwrap_or("bar");
let p = Path::new(b);

println!("{:?}", p);

#4

This looks quite complicated for something that should(?) be easy. I wonder if a Cow could somehow be an easier alternative?


#5

As Steve said, the problem is that you want to pass a reference/pointer to Path of s, and s doesn’t live past the match arm that defined it. It needs to outlive the match precisely because it’s a reference to the data in s, and s is dead once the match is out of scope - hence, the Path you create can’t actually function at all.

You could get what you want out of a PathBuf rather than a Path, as PathBuf knows how to be constructed from a String:

use std::path::PathBuf;

fn main() {
    let a = Some("foo".to_string());
    let p = match a {
        Some(s) => PathBuf::from(s),
        None => PathBuf::from("bar"),
    };
    println!("{:?}", p);
}

Assuming you really want a Path, though, what you want is to use the ref pattern match:

use std::path::Path;

fn main() {
    let a = Some("foo".to_string());
    let p = match a {
        Some(ref s) => Path::new(s),
        None => Path::new("bar"),
    };
    println!("{:?}", p);
}

This way, the variable s is a reference to the data in a from the beginning of it’s lifetime - it was never a separate, owned binding. The result is that your code works. :smile: I would say using ref is the preferred choice.


#6

I had thought about ref but for some reason it didn’t go farther than that. Nice.


#7

Thank you for all the great replies! They really helped clarify things. Getting a reference from the match pattern is exactly what I seem to have wanted.

Part of my misunderstanding was that I didn’t realize that Path used a slice rather than copying the string internally. (That’s what I get for not reading the docs carefully enough.) But this brings up one more incredibly naive question: how does the compiler “know” that Path::new returns something that needs to live at least as long as its argument? (I don’t see any obvious indication in the function’s type.)

That is, this uses Path::new to (of course) run into an error:

let p = {
    let s = "foo".to_string();
    Path::new(&s)
};
println!("{:?}", p);

But if I just replace that call with a dumb function that gets the length of the string, Rust is (rightly) happy!

fn f(x: &str) -> usize {
    x.len()
}

fn main() {
    let p = {
        let s = "foo".to_string();
        f(&s)
    };
    println!("{:?}", p);
}

My questions boils down to: how does Rust know the difference between Path::new and f with respect to lifetimes? I’m guessing this has to do with inferred lifetimes, and it would be obvious if all the lifetimes were explicit. Is that right?

I suppose it’s not surprising that it’s also possible to trick Rust into giving me an error when I don’t actually capture the reference. Changing f slightly to:

fn f(x: &str) -> &str {
    x.len();
    "bar"
}

gets the same error as Path::new. I don’t quite see what the compiler sees in this function that is more like Path::new and less like the previous f.

Thank you again for helping me through this!


#8

AIUI, without explicit lifetimes the compiler infers that input reference lifetime == output reference lifetime, i.e. as if you put an &'a str there in both instances.

This is part of the function’s type, so even though you return a value that could have a 'static lifetime you’ll get an error in the last example. This would pass: fn f<'a, 'b>(x: &'a str) -> &'b str.


#9

Here is the code for Path::new:

    pub fn new<S: AsRef<OsStr> + ?Sized>(s: &S) -> &Path {
        unsafe { mem::transmute(s.as_ref()) }
    }

The answer to your question, as I understand the system, is that the lifetimes are elided - without having to specify anything, the reference &S has the same lifetime as the reference &Path that gets returned. So the compiler figures this out relatively easily - the actual lifetime of S is the lifetime of the owner. Under the hood, you’re literally changing the type of &S to &Path - which is pretty sweet, when you realize whats going on. :wink:

The reason this:

let p = {
    let s = "foo".to_string();
    Path::new(&s)
};
println!("{:?}", p);

Is an error is that, again, the life of the String s is the block for let p = {...}. Since the return type is a &Path, and &Path is itself a reference, the value of p is a &Path - and since the value you transmuted to the Path is inside the let p block, it simply doesn’t live long enough.

Meanwhile, when you call f(&s), your return type is usize - which isn’t a reference, it’s an owned value. Hence it has its own lifetime, starting from when it was bound - in this case, let p.

So, to summarize:

  • Path::new returns an &Path that is transmuted from the &str you pass it. It is literally the same object, simply its type has changed.
  • That means the lifetime of the result of Path::new is the lifetime of the &str that created it.
  • Hence the compiler knows what to do - if the value of let p is a reference to anything, the thing it references must outlive the reference. Otherwise, you get an error. It’s awesome.

Hope that helps,
Adam


#10

Thank you all again! This all helps quite a bit. It’s especially useful to remember that the return type’s lifetime is the same as the argument type’s by default for functions with one reference-type argument and a reference-type result.