Question about "temporary value dropped while borrowed"

This is the minimal reproducible demo I've ever tried:

// snippet 1: compiled successfully
struct A();

impl A {
    fn get(&self) -> &u32 {
        &1
    }
}

let value = A().get();
println!("{}", value);

But when I turn get(&self) -> &u32 into get(&mut self) -> &u32, error occurs:

// snippet 2: compiled failed
struct A();

impl A {
    fn get(&mut self) -> &u32 {
        &1
    }
}

let value = A().get();
println!("{}", value);
error[E0716]: temporary value dropped while borrowed
   --> src/main.rs:168:13
    |
168 |     let value = A().get();
    |             ^^^      - temporary value is freed at the end of this statement
    |             |
    |             creates a temporary which is freed while still in use
169 |     println!("{}", value);
    |                    - borrow later used here
    |
    = note: consider using a `let` binding to create a longer lived value

And further, when I add a String member to struct A on the basis of snippet 1, the same error occurs:

// snippet 3: compiled failed with the same error as `snippet 2`
struct A(String);

impl A {
    fn get(&self) -> &u32 {
        &1
    }
}

let value = A(String::from("1")).get();
println!("{}", value);

I do know how to fix this error. But I'm not quite sure when this error will occur and what's the key difference between the three snippets above.

Appreciate anyone who reply.

&1 is a very special case, not really representative of how borrowing works most of the time.

This is because 1 is a constant hardcoded into your program, so it never goes away, so you can freely borrow it forever.

This is different for String, which is newly created every time, and needs to be dropped before the program ends. Same goes for variables in functions - they are created when function is called, and destroyed before the function returns. So things that come and go can only be borrowed temporarily while they exist, and will be more restrictive than a borrow of &1.


In the &mut self case two things happen:

  • methods have rules that their inputs are tied to their outputs (lifetime elision)
  • lifetime of exclusive references (&mut) is intentionally very inflexible and picky (AKA invariant)

The default for methods is get<'a>(&'a self) -> &'a i32, meaning that i32 is a field in self, but your code actually does get<'a>(&'a self) -> &'static i32 (that i32 is not borrowed from a field of self). This silently incorrect declaration is the cause of the compilation error.

As for inflexibility: when you have multiple shared & borrowed references that have different lifetimes (like &self that lives temporarily, and &1 that lives forever), Rust will silently make them equal, so everything checks out (will borrow &1 temporarily in this case). But &mut can be written to, and this enables complex edge cases where references could be incorrectly swapped around, so Rust must be strict, and won't fix almost-but-not-quite-precise &mut lifetimes for you.

2 Likes

The first case is the special one / the odd one out. It fmworks because an expression like &A() (there's an implicit borrow in A().get()) can be promoted to &'static A. This promotion can only happen for for immutable references to constant expressions. Turning it into &mut breaks the first criterion, while adding a String field and initializing it with String::from breaks the second condition. There's also additional conditions like: The type must not have interior mutability (otherwise it cannot be used as a constant expression) and it must not do anything when dropped (otherwise, there's a behavioral difference from dropping the temporary later than expected). The type with String field also violates this last condition since a the String field does execute code when dropped. And for the record, mutability (through &mut or interior mutability) is disallowed, because the promotion has the effect that every call would use the same static instance of the value, so of course this one fixed instance must not change, otherwise this would be an observable change to the meaning of the program, too.

4 Likes

Is it understandable that strictly speaking, A(String::from("1")).get() is not a constant expression?
And is the Constant Promotion related to the evaluation result of the expression(In this case, the result is &1)? In other words, is the following code sound?

    struct A();

    impl A {
        fn get(&self) -> &String {
            todo!()
        }
    }

    let value = A().get();
    println!("{}", value);

You deleted your follow-up question, here's an answer anyways:
Edit: Now the question is below this answer ^^

So the expression A(String::from("1")).get(), which is syntactic sugar for A::get(&A(String::from("1"))), contains the sub-expression &A(String::from("1")), which creates a reference to A(String::from("1")). This expression is not referring to any existing place (like e.g. a variable, or a field), so it creates new, temporary place to put the value for you. This is called a "temporary", those are like implicitly created anonymous variables that live for the duration of one statement.

The whole statement here is something like

let value = A(String::from("1")).get();

right after that statement, the temporary would be dropped, the method get couples the lifetime of the returned &u32 to the lifetime of the &A that's passed in, so the borrow checker disallows any further use of value after this let statement. What the implementation of get actually does doesn’t really matter, because functions are checker (mostly) independently in Rust, just based on the signature of other functions. (This is great for API stability and for local reasoning.)

This is the "normal" case, and naively it would happen for the example of

let value = A().get();

too. But this case can be made work anyways by constant promotion, which basically turns your code into

const PROMOTED: &'static A = &A();
let value = PROMOTED.get();

Most restrictions for when this can happen are the same restriction for when the respective const declaration would be accepted in the first place (i.e. it needs to be constructable at compile-time, which rules out functions like String::from; it cannot have interior mutability of be a &mut _ reference. There's the additional restriction that nothing must happen when dropping the variable, so that this transformation doesn't change behavior, even when it's applied to code that would've compiled successfully without the constant promotion (otherwise someone who expects the drop code to run at the end of the statement could rightfully complain about unexpected compiler behavior). This last restriction can actually be circumvented if you write the const yourself, and this way "opt out" of expecting any drop behavior. E.g. something like

// snippet 3, modified
struct A(String);

impl A {
    fn get(&self) -> &u32 {
        &1
    }
}

const C: &'static A = &A(String::new());
let value = C.get();
println!("{}", value);

does actually work. Of course in this concrete example, using an ordinary local variable would be fine, too, and more lightweight syntactically, as well as more flexible with what kind of String values you may create.


The &1 in your code examples is also an example of const promotion, but it's unrelated to the reason why some snippets compiled an others didn't.

3 Likes

Is that understandable that strictly speaking, A(String::from("3")).get() is not a constant expression, so constant promotion won't happen?
And is the constant promotion related to the evaluation result of the expression (In this case, the result is &1)? In other words, is the following code sound?

    struct A();

    impl A {
        fn get(&self) -> &String {
            todo!()
        }
    }

    let value = A().get();
    println!("{:?}", value);

I don't know what's is going on, my earlier reply just gone... :sweat_smile: I re-reply it above.

As far as my shallow knowledge, the only struct follows interior mutability pattern is RefCell<T>. So how to understand String::from has interior mutability?

No it doesn’t. String falls into this category

It has a destructor that deallocates its memory. However if you ever need to create a &'static String (which should be rare) you can still manually "promote" a value to a constant by just - well - creating a constant; which is this point:


There’s also Cell, Mutex, RwLock and a few Atomic… types in the standard library, to name some more.

I see. Thanks a lot ! :heart:

emm.. I just looked up the manual and find no Drop trait of String. Have i missed it or it was implemented implicitly by rust?

When a value in Rust is dropped, not only the code from its direct Drop implementation is executed (if that exists), but (after that) also always all the fields are dropped, too, recursively. So if String or any of its fields, or any of the field of those, etc… implement Drop, then String has a non-trivial destructor; sometimes people also would say “String has drop-glue” (because there’s some drop-behavior of its fields attached to it). See more: Destructors - The Rust Reference

The type String uses a Vec<u8> internally, which implements Drop :wink:

1 Like

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.