Ownership and borrowing

This is not a really important one, but I try to fully understand the basics. The way for me is to see the similarities between languages. I really understood pointers and (de)referencing in C when I realised “a variable in C is just like a label in assembler”.

In the same way, is it very wrong (I do understand it is not exactly right) to understand ownership as only one label can be used for one value (variable) and borrowing is highly similar to using a pointer to the value, not through the label/variable used?

Or am I talking true nonsense now?

(straining noises) ... nnnnnno. I don't think trying to liken these to labels or pointers is going to help much. I'm concerned it might end up making it harder to understand. [1]

That said, as an inaccurate generalisation, you could say that a borrow of a value is generally akin to taking a pointer to a storage location.

Ownership is primarily concerned with answering the question "who is responsible for cleaning up this value?". So, a Box<T> has exactly one owner, and that owner is responsible for destroying the value and deallocating the box. (The owner is the storage location that holds the actual Box<T> value itself.) An Rc<T> has one or more owners who are collectively responsible for clean up. A stack variable of type T has one owner: the function that defined that variable. A global of type T has one owner: the OS, which will "clean up" the storage by killing the whole process.

Borrowing can be thought of as negotiating for access to a value that you don't own. That is, if you borrow it, you are not allowed (or expected) to clean it up. You can borrow a &T from a Box<T>, or an Rc<T>, or a T on the stack, or a T in a global... and in all cases you can look but not touch (and definitely don't destroy or deallocate).

Almost all the time, a borrow will take the form of a reference (which is, under the hood, a pointer), although that might not be universally true. It is possible to have a borrow without a reference or pointer being held (that's typically for things like guards where you are logically tying the guard value to a borrow, even if there's no actual pointer involved, or resource handles that are managed elsewhere).

So the analogy doesn't really work because a value can be owned by more than one variable, and borrowing doesn't necessarily involve any pointers. And also because you can own something via a pointer (like Box or Rc).

Does any of that help?


  1. (Note: translating Rust concepts into other languages can be dicey as there are a few places where a Rust concept looks similar to something in another language, but with critically important differences. A common one is "traits are interfaces", which I've seen trap many people. Just be cautious you don't over-generalise.) ↩︎

5 Likes

A "borrow" (reference) is a pointer.

A "variable" (named binding) in Rust is an identifier that designates a memory region. It's not a pointer, because it represents the memory region/place/value itself, but most memory operations (that don't end up being optimized into registers) are necessarily formulated in terms of pointers anyway.

3 Likes

I don't think that's a helpful PoV. A variable of type Rc<T> always has exactly one owner, as usual. When that variable goes out of scope, it is dropped. There is also a resource attached to that variable, i.e. the reference-counted data, but since that resource is managed via raw pointers and unsafe code, it's not directly related to the ownership rules. As far as the compiler is concerned, managing that data is some black magick outside of its comprehension.

The specific unsafe code in this case implements "shared ownership", but that concept is outside of Rust's normal rules. A different unsafe data structure can implement entirely different ownership rules, such as garbage collection, or some static data with access controls, or whatever you can think of.

2 Likes

yes, that helps! Thank you

I love the idea of compiler black magic by the way

It might not be possible to fully understand the basics right now :smiley: - see https://doc.rust-lang.org/nomicon/references.html for details.

Consider the following example:

fn main() {
    let mut a = 42;
    let r1 = &mut a;
    let ptr1 = r1 as *const _;
    let r2 = &mut *r1;
    let ptr2 = r2 as *const _;
    assert_eq!(ptr1, ptr2);
    // &r1; // ILLEGAL, r1 is already borrowed
    *r2 += 1;
    *r1 += 1;
    println!("{a}"); // 44
}

If you uncomment the line, the compiler will complain that r1 is already borrowed, but note that nothing ever points at r1 (this is actually called reborrowing, a very common concept in Rust).

1 Like

I would be very cautious about comparing Rust references to C pointers. This analogy doesn't explain the difference between references and owned values, because Rust can have owned values that are pointers too.

Rust can also have values with lifetimes that are restricted the same way references are, but are not pointers (structs with inner references or PhantomData). And Rust has "fat pointers", like &str which technically is a struct with a pointer and a length.

So pointer vs not-pointer is a different axis than reference vs not-reference. C uses pointers to avoid copying, but Rust uses references/loans to limit scopes of values and avoid transferring ownership. These are different concerns.

In case of Box<T> vs &T there exists an analogy that can be explained in C terms:

  • Box<T> is a pointer, equivalent of T *box = malloc(sizeof(T)) (but can use a different allocator than libc's). The last user of the box pointer calls free() on it, and Rust automatically makes that happen.
  • &T is also a pointer. In memory it's 100% identical to Box<T>. However, it's more like C's const T*, and functions handling &T are never ever allowed to free() it.

To me Rust's references are more like read/write locks that give temporary permission to access a value, except the locking mechanism is evaluated at compile time.

And ownership is a memory management strategy. It's a little bit like automatic reference counting, but limited to a special case that the reference count can only be 0 or 1.

3 Likes

No, this is not true. Indirection serves the same purpose in both languages. Indirection is a fundamental necessity in programming, and it doesn't really matter whether you call them "references" or "pointers".

It's therefore a huge misrepresentation to say that "C uses pointers to avoid copying". That's not nearly the only (or the most important) use of pointers. They are pervasive in solving various problems, and most of the time it has nothing to do with avoiding copying. A linked list is impossible to implement without indirection, for example.

1 Like

I think part of disagreement here is a disguised query. References compile to pointers and have common uses with C pointers, but also they have additional responsibilities/limitations that pointers don't. So whether "it's a pointer" is true for you depends whether you see where it overlaps with pointers, or whether you look where it doesn't.

I try not to say references are pointers, because of the places where they differ. In practical terms, it causes C programmers to get stuck on code like:

Object *make_new_object();

where use of the pointer makes sense. But if you think "references are pointers" then this attempt to "return by reference" will get you stuck:

fn make_new_object() -> &Object

C also uses pointers for uniqueness, because structs are copyable (e.g. if you implement a C equivalent of a growable Vec, then you're going to use Vec*, as otherwise you risk growing a copy). In Rust unique ownership makes that indirection unnecessary.

C does have a culture of avoiding copying. Libraries will rather have init(struct*) than struct init(). Types may be self-referential, and in general a C programmer can't assume that changing struct's address is safe (e.g. see pinned init acrobatics that Rust in Linux kernel has to do). Rust has movable types and fn new() -> Self.

Rust's types without &/* look like copying to C and C++ programmers, and they may try to use &String rather than String to avoid "copying the string".

3 Likes

The concept of pointers is much more widely relevant than just C. Granted, many people learn pointers in C, but that's because C is the most widespread systems language. The general idea of a pointer is not specific to C, and I intentionally didn't write that Rust references are like C pointers. They implement the abstract concept of a pointer, just like C pointers or Java references or <insert language here> do.

But then the correct equivalent return type is Box<Object>. Box is also a pointer. It's properties that are specific to Rust are of course different from references, but it still implements the general idea of "a pointer".

Pointer-ness isn't somehow intrinsically intertwined with C. You don't seem to pick equally as much on arrays or structs. Both constructs exist in C, and they have their own subtletied in C, and their exact details differ between C and Rust. Yet it sounds silly to assert that "Rust arrays are not arrays" or "Rust structs are not structs". Yes, they exist in C. Who cares? This is a different langauge, and of course there will be differences. Recognizing a general pattern and naming it isn't harmful.

2 Likes

When people transfer their intuition about structs and arrays from other languages, they generally manage to use them correctly (with exception of structs with references!)

But that doesn't happen with references. When people think "oh, I know references in Java, so I know how to use references in Rust", they get stuck. They put an object "by reference" in a struct, like they would in Java, and have lifetime annotations explode all over the code. They want to have parent-child relationships via references, like in Java, and it makes borrow checker send them in circles. They want to make event handlers based on references, like in Java, and nothing lives long enough.

So while "address of an object elsewhere in memory" is fundamental and shared by all these concepts, there's much more to it. The "twist" that Rust adds to the concept of general-purpose references/pointers is for most people something completely new.

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.