3

Given the following function definitions:

// A: Compiles with static lifetime
fn foo() -> &'static i32 {
    let i = &42;
    i
}

// B: Compiles with normal lifetime
fn bar<'a>() -> &'a i32 {
    let i = &42;
    i
}

// C: Does not compile but same as B
fn baz<'a>() -> &'a i32 {
    let i = 42;
    &i
}

// D: Does not compile either
fn qux() -> &i32 {
    let i = &42;
    i
}

I would expect all 4 of these to fail compilation because we are returning a reference to a local variable but to my surprise A and B both compile correctly, yet C and D do not.

For A: How is it possible to use 'static here because static data lives for the lifetime of the program and this data is created dynamically (possibly multiple times) during program execution?

For B: Why is this allowed and what is the lifetime of 'a bound to - is it implicitly 'static as well? Why does this no longer work when changing to types such as String?

For C and D: Why do these not compile when they are effectively identical to B?

Do any of these create dangling pointers, or is something going on behind the scenes?

Do they create memory leaks because the returned value lives forever?

  • 1
    You can never create dangling references in Rust (unless you use `unsafe` code). That's one of the great things about this language. – Thomas Aug 25 '22 at 11:51
  • 2
    This is because of [rvalue static promotion](https://rust-lang.github.io/rfcs/1414-rvalue_static_promotion.html). To expand on this, in A and B `i` already has type `&'static i32` so you are simply returning a reference to some static data. In C, you are attempting to return a reference to data owned by the function which is correctly disallowed. D fails to compile for a different reason altogether. – apilat Aug 25 '22 at 11:55
  • That makes a little more sense, thanks. I guess the `'a` is a masked `'static` then? And D fails to compile because it does not meet the lifetime elision rules? – Louis Quinn Aug 25 '22 at 12:04
  • 1
    @LouisQuinn Exactly. Because in B `i` is `'static` you can plug any lifetime in for `'a` and get a value that lives long enough. And a return parameter can't elide its lifetime unless there is exactly one unnamed input lifetime parameter with the same mutability, c.f. [the nomicon](https://doc.rust-lang.org/nomicon/lifetime-elision.html). – isaactfa Aug 25 '22 at 12:08

1 Answers1

2

To pull together all that has been said in the comments in a single answer:

Case A

fn foo() -> &'static i32 {
    let i = &42;
    i
}

Here, because of rvalue static promotion, 42 is not allocated on the stack, but instead is considered as a const variable. This means that it will actually be shipped with the binary produced by the compiler, and the pointer to it will be a pointer to literally a value that lives as long as the program (because it is a part of it).

Case B

fn foo<'a>() -> &'a i32 {
    let i = &42;
    i
}

This example is very similar to the previous one, with a slight modification. Instead of a 'static lifetime, we instead want this time a generic 'a. This works because

  1. By definition of 'static, 'static: 'a.
  2. &'a T is covariant in 'a.

This means that the compiler is (quite intuitively) allowed to "demote" a lifetime that is always valid to a lifetime that is valid when I ask it to be (because that's "included in always" in a way). More information on variance can be found in the rustonomicon.

Case C

fn foo() -> &'static i32 {
    let i = 42;
    &i
}

Again, a slight modification. Or it may seem so. The big difference is that this time we explicitly bind 42 to the function's scope. Because of this, it will be allocated on the stack, and will be freed when the function returns, thus the compilation error. This is the regular prohibited dangling pointer error you mentioned.

Case D

fn foo() -> &i32 {
    let i = &42;
    i
}

This does not compile for a very different reason. In fact, you could even forget about the function's content, and it would still not compile:

fn foo() -> &i32 {
    panic!()
}

This is because, in general, all lifetimes must be explicit. There are a few cases where the compiler has some rules that will auto-assign these lifetimes, so they're allowed to be implicit (note that this does not mean that the compiler will figure out the "right" lifetimes, as it would with types; it can produce "wrong" lifetimes, which will prevent your program from compiling). The exact rules can be found in the reference. Also note that, even though I said in general lifetimes must be explicit, it turns out in practice the few lifetime elision rules are both enough and often right. Handy!

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
jthulhu
  • 7,223
  • 2
  • 16
  • 33