Why does this code raise E0716 (temporary value dropped while borrowed), even with clone()?

Hi all!

I know this specific error has been asked thoroughly (I apologize for the repetition!), but I still can't understand while this error raises an error.

use num_bigint::{BigInt, ToBigInt};
use std::{cmp::Ordering, ops::Add};

impl Add for Decimal {
    type Output = Decimal;

    fn add(self, rhs: Self) -> Self::Output {
        let lmsd = self.mantissa.to_str_radix(10).chars().nth(0).unwrap().to_digit(10).unwrap(); //should be ok since decimal numbers are ASCII
	let rmsd = rhs.mantissa.to_str_radix(10).chars().nth(0).unwrap().to_digit(10).unwrap();
	let mut res_mantissa = self.mantissa + rhs.mantissa;
	let mut res_integral = self.integral + rhs.integral;
	let res_msd = res_mantissa.to_str_radix(10).chars().nth(0).unwrap().to_digit(10).unwrap();
	if res_msd < lmsd || res_msd < rmsd { // overflow
	    res_integral += 1;
	    let res_str = res_mantissa.to_str_radix(10).chars().as_str();
	    res_mantissa = res_str[1..res_str.len()].parse::<BigInt>().unwrap();
	}
	Decimal{ integral: res_integral, mantissa: res_mantissa }
    }
}

It comes from an Exercism's exercise. cargo check returns

cargo check --benches --tests --all-features 
    Checking decimal v0.1.0 (/home/alessandro/exercism/rust/decimal)
error[E0716]: temporary value dropped while borrowed
  --> src/lib.rs:29:20
   |
29 |         let res_str = res_mantissa.to_str_radix(10).chars().as_str();
   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                 - temporary value is freed at the end of this statement
   |                       |
   |                       creates a temporary which is freed while still in use
30 |         res_mantissa = res_str[1..res_str.len()].parse::<BigInt>().unwrap();
   |                        ------- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

IIUC, to_str_radix() gives a String, which is created but not bound to a variable, since in res_str I actually save chars().as_str(). However, this retruns a &str, a reference to a memory area containing a String. So first question is: why is the value dropped, even if I could still access that memory (I'm even in the same block)? My guess is that the String object is allocated on the heap, but since it is not saved anywhere it is immediately deallocated. However, having a reference to it, that shouldn't be dropped, should it?

Then, trying to fix it, I found that adding at the end of line 29 to_owned() solves the problem (since it creates a copy of the String, and in fact now the type of res_str is not &str, but String). However, clone() doesn't work. Why? Is it because it copies the reference, and not the string (I see the type in that case remains &str)?

Can you please tell me if I understood everything correctly, or if I got something wrong?

Thanks! :slightly_smiling_face:

Note: this is a really good question! You've precisely identified the behavior that's confusing you, what you had expected to happen, and why, and the point of confusion gets to the heart of how Rust's borrow check works.

It's the other way around -- because the temporary String is dropped (conceptually) at the end of the let statement, no references (&T, &mut T) to it may be active after that point. The existence or nonexistence of references to a value never determines when that value is dropped. Instead, the drop happens when the owner goes out of scope or is overwritten (or at the end of the statement for temporary values that have no owner), and your use of references has to accomodate that fact, otherwise the compiler will detect that your reference would dangle.

To get the semantics you describe, where any extant "reference" (in this case a broader sense of the word, not just &T and &mut T) keeps the value alive, you can use shared ownership, as implemented by a reference-counting smart pointer like Arc<T>.

When the last Arc pointer to a given allocation is destroyed, the value stored in that allocation (often referred to as “inner value”) is also dropped.

2 Likes

Because Rust doesn't have a Garbage Collector. Usually it's a job of a GC to check if there are any outstanding references and keep values alive for longer, if it's necessary. Because Rust doesn't have a GC, it can't dynamically check it. It can't make any object live longer. Objects only live for as long as outcome of their moves and scopes, based on static unforgiving rules. When objects go out of scope, they're dropped, and references can't prevent that.

Instead, it's your responsibility to write code and use references in a way that doesn't interfere with the objects being dropped anyway. Because of that "references" in Rust are not equivalent to references in other languages. They're not a general-purpose way to refer to objects, they're only temporary scope-bound permissions to view data.

2 Likes

Did you try applying the suggestion? This compiles:

        if res_msd < lmsd || res_msd < rmsd {
            // overflow
            res_integral += 1;
            let res_string = res_mantissa.to_str_radix(10);
            let res_str = res_string.chars().as_str();
            res_mantissa = res_str[1..res_str.len()].parse::<BigInt>().unwrap();
        }

Before doing this, to_str_radix(10) created a temporary value (a String) that only lasted until the end of the statement. By storing the String in a local variable, it lasts until the end of the if block.

More on drop scopes and temporary scopes can be found in the reference. (Granted, it's pretty dense reading.)

More adjustments.

There's no reason to call chars just to get a &str, you can index a String directly just as you can a &str. You actually don't need to query for the length either, you can use 1.. to mean "until the end."

        if res_msd < lmsd || res_msd < rmsd {
            // overflow
            res_integral += 1;
            let res_string = res_mantissa.to_str_radix(10);
            res_mantissa = res_string[1..].parse::<BigInt>().unwrap();
        }

This can all happen in one statement and you won't need to assign the temporary to a variable anymore.

        if res_msd < lmsd || res_msd < rmsd {
            // overflow
            res_integral += 1;
            res_mantissa = res_mantissa
                .to_str_radix(10)[1..]
                .parse::<BigInt>()
                .unwrap();
        }
2 Likes

:heart:

Ah-ha, I think this is the key point, thanks.

That's a wonderful suggestion, however I'm not to that point of the book yet, so I can only more-or-less understand the effectiveness of Arc/Rc by my university course on distributed systems. :sweat_smile: Anyway I'll try to check it.

I remember the book describing how the compiler automatically checks for the usage of references, but now that I look back to it I realize it checks for reference types, not underlying data storage life. Thanks!

The point is that I didn't understand what was the value to be kept. I kinda guessed it, but it made me wonder if I could avoid using a variable and write cleaner code. Which leads me to your other suggestion:

These two suggestions are extremely useful, thanks!

P.S. thanks to you people, wonderful community!

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.