Why not automatic inference of 'static in foo() -> &str?

Hi,

I recently came across this error:

fn foo() -> &str {
  return &"bar";
}

That required me to add a 'static as such:

fn foo() -> &'static str {
  return &"bar";
}

This is fine, except it is confusing why the compiler doesn't do this automatically. In particular, it seems to me that zero-parameter functions that returns a reference can only possibly return 'static references, so there should be a rule like the lifetime elision rules that adds the 'static in. Am I missing something?

And apologies if this is answered elsewhere -- the various searches I could think of produced no results.

Thank you.

fn foo<'a>() -> &'a str {
    "Foo"
}

is also valid code - it's similar but not the same as the 'static version, as instead of returning a static string it returns a string of any lifetime the caller wants. So 'static isn't the only possible desugaring of the code.

It is true that Rust could support desugaring to the 'static version or this version for functions without lifetimed parameters, but it's a fairly uncommon case and there's no real harm in having to be explicit about it.

1 Like

Thanks! That helps.

I think my confusion here stems from the elision rules: it seems strange that this works:

fn foo(x: &u32) -> &str { ... }

but this doesn't:

fn foo() -> &str { ... }

See also, lifetimes Ellison rules

Exactly. Without knowing the lifetime elision rules and all the details of how they're applied, the above can be very confusing. Even if you do know them, the above two cases seem to fill the same conceptual space (at least for me and people I've been talking to), but the rules' technical conditions work hard to make sure they don't apply to the second case.

I'd have included a rule for the second case to prevent this kind of confusion, and I was searching for reasons why they didn't. My experience with Rust has been that they generally have solid reasoning for decisions like these.

The fact that there are two ways of de-sugaring things is the beginning of an answer, but it still seems clear that the foo<'a>() -> &'a str de-sugaring is always a fine thing to do. Maybe it isn't a common enough idiom to care about, but I was hoping for a stronger explanation.

Not sure what you mean, since the desugaring of these example are both very similar.

fn foo<'a>(x: &'a u32) -> &'a str { ... }
fn foo<'a>() -> &'a str { ... }

Each elided lifetime is still caller-defined, e.g. neither uses the special 'static lifetime. So I guess it depends on what you mean by the first example "working" and in what context, contrasted to the latter.

This comment is an example of the kind of confusion the current rules create. I made the same mistake of assuming that:

fn foo() -> &str { "hi" }

would compile, desugaring to something reasonable (such as what you suggest which is better than my 'static insertion).

The fact that "foo(&u32) -> &str" desugars properly suggests that will work.

That is not what happens. See: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=03738f01f1268da7046ccc271d2ec899

You could say that we just need to understand the lifetime elision rules then we won't make that mistake, but I often see the Rust team with an interesting sometimes subtle argument when they create potentially confusing situations like this.

I think the important point here is not about whether this could be done -- I don't think it would break anything to add this elision rule -- but whether it would be helpful enough to be worth it overall.

To me, &'static is rare enough that it's worth having it be mentioned explicitly in the situations where it's used. It's also a good practice to be used to writing it out so that if you put a method like fn variant_name(&self) -> &'static str you're more likely to remember the 'static, rather than just -> &str and accidentally returning something with a much shorter lifetime that you mean when that's probably returning a string constant.

Or, on the other side, it's easy to be tempted to write fn fizzbuzz(x: i32) -> &str, but that just isn't a good signature for that. I rather like that it's an opportunity for the compiler to hint that "this function's return type contains a borrowed value, but there is no value for it to be borrowed from" -- and just nudge the user towards returning an owned value instead in such situations.

EDIT: Filed an issue to also suggest -> String here: https://github.com/rust-lang/rust/issues/76007

2 Likes

Another related thread (Why are lifetime explicit?) just linked to https://gankra.github.io/blah/only-in-rust/#unbound-lifetimes which has another good example.

It's helpful that this code doesn't compile:

fn foo(input: *const u32) -> &u32 {
    unsafe {
        return &*input
    }
}

Because -> &'static u32 is almost certainly wrong, so allowing it to elide to that would be unfortunate.

3 Likes