I’m going through the interactive version of The Book, and I’m confused by the results of an exercise in Ch 4.3 - Fixing Ownership Errors.

The following code does not work, and they say it’s because it would result in the same heap space being deallocated twice:

fn main() {
    let s = String::from("Hello world");
    let s_ref = &s; // reference s
    let s2 = *s_ref; // dereference s_ref
    println!("{s2}");
}

But in my mind, this should be equivalent to the following compilable code, which transfers ownership of s to s2 :

fn main() {
    let s = String::from("Hello world");
    let s_ref = &s; // reference s
    let s2 = s; // move s directly
    println!("{s2}");
}

If s_ref is a reference to s, then dereferencing s_ref should return the String s, shouldn’t it? Why can’t s be moved to s2 with either the above code or let s2 = *&s;, which fails in the same way?

  • Max-P@lemmy.max-p.me
    link
    fedilink
    English
    arrow-up
    12
    ·
    1 year ago

    There can only ever be one owner of the value, otherwise the compiler can’t do its magic to guarantee that all references are dropped before the owned value. The owner is responsible for freeing the memory when it’s done, so having two owned values of the same thing would indeed result in a double-free. Therefore, you can never upgrade a reference to an owned value. If you ever need to own the value, you need to either copy it, or own it by moving it. Or live with just a reference to it, which you’re only permitted to have as long as the owned value is live. That’s the lifetime concept that gets tracked.

    In your second example, it only works because the compiler knows you’re done with the reference, so you can move the value. If you try to print s_ref in your second example after you moved s into s2, it wouldn’t compile because the reference has been invalidated by the move. After the move, s2 could be in a different memory address than s was, so s_ref points to memory that could have been reused for something else. You can’t have that, that’s a memory bug, and Rust doesn’t allow you to do that, it’s what it’s designed to do.

    Yes let s2 = *&s makes sense logically, but in practice you’re still going owned->reference->owned without moving ownership of the original s, so it’s not permitted. The example is trivial to optimize out, but what if that reference gets passed down a whole bunch of functions, potentially in a dynamic library the compiler has no visibility into at compile time? It can’t possibly track than and guarantee memory safety. But if you only pass references to the dynamic library, it knows by the time the function call is done and you’re out of the library, nobody’s using the value anymore and is therefore safe to drop. The lifetime annotation (implicit or explicit) guarantees that the called function is not allowed to keep the reference around, because it’s only borrowed and your main function wants its value back.

    The typical workaround for this is an Rc or Arc. Those add extra logic so that the value is only dropped once, when all owners have dropped the Rc, then the Rc knows it can safely drop the inner value as well.

    With Rust, always think in terms of “who owns this value”. There’s only one owner, and one or many borrowers. Like physical items, when it’s borrowed, you don’t technically have it even though you own it. You have a reasonable expectation that the borrower will return the item back to you, and only then you’re free to change it again. Otherwise it could result in the called function to see a surprise change of the value while it has an immutable reference to it!

    • b_van_b@programming.devOP
      link
      fedilink
      English
      arrow-up
      1
      ·
      1 year ago

      Thanks for the detailed explanation! I guess the main takeaway is that a reference can never be converted back into an owned value. Later content in the book also gave more information about when you have the right to transfer ownership.