Simple questions about lifetime and reference

In my programming journey, I did not have to deal with pointers, references, and lifetime. However, this is a different game when I am trying to use Rust in my personal project. I have some confusing thoughts and questions about references and lifetime in Rust.

Here is the simple code snippet :

//code snippet 01
fn main()
{
    let a = 100;
    let b = &200;
    r(&a);
    r(b);
}

fn r(a: &i32)
{
    println!("{}", a);
}

Now I am going to put static lifetime in function r:

//code snippet 02
fn main()
{
    let a = 100;
    let b = &200;
    r(&a);
    r(b);
}

fn r(a: &'static i32)
{
    println!("{}", a);
}

But the code snippet 02 does not compile. From my understanding, it seems that b has a static lifetime but the &a does not. We did not mention the static lifetime explicitly while declaring the b.

Why does b have a static lifetime while &a does not?

1 Like

This comes from a feature called “rvalue static promotion” or “constant promotion”. It’s rather complex (and somewhat underdocumented) to list the exact conditions of when it happens, and for code like let b = &…something…;, to make matters more complex, Rust has a second, different “promotion” feature (called temporary lifetime extension).


Let me try to explain nonetheless what each feature does:

Without either special feature, code like

let b = &200;

should seem “problematic” in principle: If you think about it under Rust’s basic semantics, it shouldn’t work at all! The 200 gets evaluated in temporary variables and that temporary variable is dropped at the end of the let statement. The reference should be immediately dangling. You can reproduce this kind of behavior in actual Rust code, if you wrote something like

fn do_nothing<T>(x: T) -> T { x }
fn return_200() -> i32 { 200 }

let b = do_nothing(&return_200());

which is functionally pretty much equivalent to let b = &200; (after all, do_nothing is a no-op, returning its argument, and return_200 evaluates to 200) but this more cumbersome version of the code manages to avoid both kinds of lifetime extension rules I’m about to explain.

The code still compiles, the reference b is immediately dangling, but never used – if you do use it however, as in

fn do_nothing<T>(x: T) -> T { x }
fn return_200() -> i32 { 200 }

let b = do_nothing(&return_200());
println!("{b}");

then you get the (expected) compilation error

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:3:21
  |
3 | let b = do_nothing(&return_200());
  |                     ^^^^^^^^^^^^ - temporary value is freed at the end of this statement
  |                     |
  |                     creates a temporary value which is freed while still in use
4 | println!("{b}");
  |           --- borrow later used here
  |
help: consider using a `let` binding to create a longer lived value
  |
3 + let binding = return_200();
4 ~ let b = do_nothing(&binding);
  |

(Rust Playground)

and the fix (as suggested by the compiler) is to first store the value returned from return_200 in a local variable, not an implicit temporary variable.

Temporary Lifetime Extension

Without do_nothing, i.e. with the code

let b = &return_200();
println!("{b}");

the error suddenly goes away. How? This is due to “temporary lifetime extension”. Code of the form let x = &…; will always create an entirely unusable reference to a local variable, and thus as a convenience feature, Rust was designed such that in this case, the compiler essentially applies the same fix as it suggested above, automatically and implicitly; so it re-writes your code into the something like the following:

let local = return_200();
let b = &local;
println!("{b}");

This code still has b containing a reference whose lifetime is bound to the call stack of the function / the time the local variable is alive, so it isn’t immediately dangling anymore… But as you can see, this is the same situation as with your r(&a) example; borrowing from a local variable (like a) still doesn’t allow for a 'static lifetime, as for 'static lifetime, you can only borrow data that lives in global variables, or memory allocated (as part of the program code, essentially) at compile-time or leaked memory.

Constant Static Promotion

As another convenience feature, we finally come to constant static promotion. The key ingredient is that the expression 200 is known to the compiler to be easily executed at compile time (i.e. a const expression) and evaluates to a value without interior mutability; and furthermore this evaluation can happen without any observable effects (such as panics or infinite loops…) and the value (an i32) being dropped is also irrelevant… all of this culminates in the observation that the 200 needs not live in a local variable at all. It can be moved into static memory instead.

The compiler essentially re-writes your code

let b = &200;

into something like[1]

static B_VALUE: i32 = 200;
let b = &B_VALUE;

and such a re-write will not negatively (or even noticeably) affect any code that worked without the re-write, but it can result in (minor performance benefits, and) more code being allowed, as the lifetime of this reference is now 'static. Which is why your call to fn r(a: &'static i32) ultimately compiles.


  1. this re-write illustrates best what is actually happening, but note that this kind of re-write cannot actually be done in all instances. The constant static promotion works even in some cases where generics are involved in the type of the value, whereas static variables cannot be generic. ↩︎

3 Likes

TLDR: You can't borrow a local variable for 'static, but some literals can be promoted to statics and thus references to them can have the 'static lifetime.

Integers are one such type of literal. Literal &str like "this" are another.


I don't agree with the impression this gives, as it's not problematic in practice.[1]


  1. i.e. if I was a newcomer I think my takeaway would be "don't do that" ↩︎

2 Likes

Maybe a wording issue? I did not at all mean to express it’s bad code. I only mean to say that this code only works because of the special rules of constant promotion, and without special rules the code is “problematic” in the sense that it cannot compile successfully, unless b is unused.


Update: applied some re-wording, hope it’s clearer now :slight_smile:


Literal &str is different though in that the literal itself is already the 'static reference, so no additional mechanism is needed.

3 Likes

Yeah, just wording or presentation -- the lead-in was "is problematic (in principal)" and the "works fine" comes after a lot of exposition which might be a lot to absorb[1].


  1. while still being valuable ↩︎

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.