this post was submitted on 01 Jul 2023
19 points (100.0% liked)

Rust

6023 readers
1 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

[email protected]

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 1 year ago
MODERATORS
 

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?

top 3 comments
sorted by: hot top controversial new old
[–] [email protected] 14 points 1 year ago (2 children)

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!

[–] nous 1 points 1 year ago* (last edited 1 year ago)

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.

It is possible it could track that. Might not be easy to do, and might slow down compile times. But even if it could IMO it shouldn't. One of the things I really like about rust is all details about what a function can do are in the function signature. There are many things they could infer from the body of the function but do not (such as the return type). IMO this makes it far easier to read rust code as you don't need to understand the body of a function to understand what can happen to the values you pass into it and get back from it.

If this were allowed it would be trivial to break backwards compatibility of a library without any signature change - by having one version not take ownership of the value through the reference, then have a client rely on that behaviour, only to change the body to do a dereference and break the clients code.

Currently in rust you cannot break downstream code without changing the function signature. Which is a very nice feature of rust to be able to keep backwards compatibility in libraries without as much effort on the developers side.

And while it could do this if you don't cross a function boundary, I still think it shouldn't. For consistencies sakes. It adds far more cognitive load when you have features that work in some places but not others and becomes yet another rules you need to learn about what you can and cannot do.

[–] b_van_b 1 points 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.