Simple lifetime question about single reference in, reference out

I'm still struggling with this little example

This passes without the need for lifetime specifiers

fn myfn(x: &i32) -> &i32 {
  x
}

Changing the argument type from &i32 to i32 causes the compiler to complain:

this function's return type contains a borrowed value with an elided lifetime, but the lifetime cannot be derived from the arguments, consider giving it an explicit bounded or 'static lifetime

Is this determined by a set of more fundamental rules? Or did somebody just figure out that fn(x: &T) -> &T just never needs lifetime specifiers but fn(x: T) -> &T does and those patterns are hardcoded into the checker?

From the Orielly programming rust book:

When a function takes a single reference as an argument, and returns a single reference, Rust assumes that the two must have the same lifetime.

From The Rust Programming Language:

When returning a reference from a function, the lifetime parameter for the return type needs to match the lifetime parameter for one of the parameters. If the reference returned does not refer to one of the parameters, it must refer to a value created within this function, which would be a dangling reference because the value will go out of scope at the end of the function.

I take that quotes to mean that in that situation the input and output are always related, but that is not necessarily true, the following passes too:

fn myfn(_: &i32) -> &i32 {
  &1
}

Related question:
If these are just function signatures patterns the borrow checker is looking for, how many more are there?

Lifetimes answer question "where was it borrowed from?" and it has to trace back to an owned value.

So with

foo<'a, 'b>(arg1: &'a i32, arg2: &'b i32) -> &'a i32

you can trace all 'a's to see that the returned reference has been borrowed from arg1 (and not arg2).

but with:

foo<'a>(arg1: i32) -> &'a i32

it says it was borrowed from 'a, but where is 'a?

A borrowed value can't exist without an owned value it was borrowed from. And it couldn't be borrowed from anything created inside the function, because that's not possible. Everything created in the function (that isn't saved somewhere else outside the function) is dropped when the function ends (the stack unwinds).

This example introduces two additional features: subtyping (aka variance) and static promotion.

Subtyping situations, such as this one, allow substituting a longer lived lifetime for a shorter one.

Static promotion takes that constant 1, forms it into a static variable, and returns a reference to it. In effect, &1 has the type &'static i32.

With those two features, the code compiles. The caller of that function, however, doesn’t know that a static reference is returned. As far as borrowck is concerned, the returned reference is tied to the lifetime of the incoming &i32 argument. So the “output lifetime is associated with the input lifetime” is true - from the caller’s perspective.

2 Likes

The rules are defined by RFC 141:

https://github.com/rust-lang/rfcs/blob/master/text/0141-lifetime-elision.md

You can also fine some additional documentation in the nomicon: