Rvalue lifetimes


#1
fn main() {
    let mut foo = &mut String::from("hello");
    foo = &mut String::from("world");
    println!("{}", foo);
}

Above code generates lifetime error complaining rvalue String::from("world") does not live long enough. This is consistent with what C++ does where the rvalue only lives for that one line. But Rust does not complain about below:

 fn main() {
    let mut foo = &mut String::from("hello");
    println!("{}", foo);
}

where the rvalue at least lives until the println line finishes.
Although you probably shouldn’t write code like this, but this looks kind of inconsistent in how to treat rvalue lifetimes.

There is some discussion about this. But I can’t find any rationale in handling them differently. Is this intended?

Somewhat related, we can rebind to new variable (not shadowing) as expected:

fn main() {
    let mut foo = String::from("hello");
    foo = String::from("world");
    println!("{}", foo);
}

where String::from("hello") is destroyed when foo is bound to another string.


#3

I don’t know the underling rules for sure, but looks like Rust and C++ behave pretty similarly here.

There is “temporary lifetime extension” thing in C++ which says that when you bind a const reference to a temporary then the lifetime of this temporary is extended. Looks like the same works for Rust, even in the case of non-constant references.

So when you let foo = &mut calculate(), then foo is initialized with a temporary whose lifetime is extended accordingly.

In contrast, when you foo = &mut calculate() you are assigning a reference and not initializing it. So the lifetime of the temporary is not extended. It’s interesting to note that C++ forbids this reference rebinding altogether.


#4

That makes sense.
I am not sure about “assigning a reference” vs “initializing” though. It looks to me as a rebind but not assigning, just like in

fn main() {
    let mut foo = String::from("hello");
    foo = String::from("world");
    println!("{}", foo);
}

foo in the second statement changes to point to another variable String::from("world"), which is a different object in another memory location. String::from("hello") is dropped at the second statement, and String::from("world") lives to the end of main block. On the other hand, assigning means that the foo would still point to variable String::from("hello"), as the assignment operator in C++ does. In this case the lifetime of String::from("hello") would continue to live to the end of main block, while String::from("world") would just be a temporary.


#5

Yeah, perhaps my word choice was not the best. Here is a less hand wavy example:

#include <string>
#include <iostream>

int main() {
    std::string hello = "Hello, ";
    std::string world = "World!";

    // You can rebind (assign) the pointers
    std::string const* ptr = &hello;
    ptr = &world;

    // But you can't rebind (assign) references in C++ (and can in Rust)
    std::string const& ref = hello;
    // ref = world;

    // Although you can assign to the referred object 
    std::string& mut_ref = hello;
    mut_ref = world;

    return 0;
}

In Rust, you need an explicit deference to get C++ style assignment:

fn main() {
    let xs = &mut "Hello".to_string();
    *xs = "World".to_string();
}

Syntactically, Rust references are much closer to pointers.


#6
fn main() {
    let mut foo = &mut String::from("hello");
    foo = &mut String::from("world");
    println!("{}", foo);
}

I don’t quite see the difference between first binding and second binding though. Maybe the second binding should also extend the temporary rvalue’s lifetime, and make it compile?


#7

There is certainly a semantic difference: in the first case you declare a new variable, in the second case you mutate the existing binding. I think that it should be possible to do lifetime extension in the second case, but I don’t think that it will be generally useful, and there may be some tricky corner cases with, for example, references to references, or references which are not local variables, but parameters of functions or memebers of structs.


#8

Thank you for this matklad. Very informative.

In contrast, when you foo == &mut calculate() you are assigning a reference

This part lost me–where is the assignment? Or did you just mean comparing? (Then the fact that the lifetime is not extended makes sense to me. The temporary survives long enough to make the comparison, but that’s about it, AFAICT.)

Also, any recommended reading on Rust temporary lifetimes would be most welcome (I’ve been encountering some compile errors which I don’t expect to be seeing).


#9

Typo, it should be a single =! Thanks for spotting it!

Also, any recommended reading on Rust temporary lifetimes would be most welcome

Looks like there’s a chapter in the reference: https://doc.rust-lang.org/reference.html#lvalues-rvalues-and-temporaries


#10

Thank you @matklad–not sure how I missed it in the Book.