Blog Post: Lifetime Parameters in Rust

I know you've been trying to understand Rust before writing it, but you might want to actually work through the book and write some code. This stuff is really fundamental, and working through it would really help you understanding here.

5 Likes

And this?

fn eat(_: &Box<Cookies>) {
    // ...
}

Is that:

If yes, then a Box<Cookies> can't be passed as an input to the following?

fn eat(_: &Cookies) {
    // ...
}

Wanting to understand the difference in meaning between the two function variants.

Have you honestly learned nothing from me in this thread? If no, did you learn nothing from the other thread I created and do you expect to learn nothing from my ideas in the next thread I am about to create before I code any more Rust? Just like to get you on record. :wink:

Where is the chart from my upthread post in the Rust documentation?

Note my upthread edit:

Both functions do not take ownership; they borrow, that's what the & means. The first one borrows a Box, the second one just borrows the Cookies. Wanting to specifically borrow a Box of Cookies is rare, but perfectly well-formed.

Also, it is perfectly possible to call the second function when what you have is a box. Box has a function to get a reference to the boxed data (it's actually an implementation of the Deref trait, but that's neither here nor there).

Okay so the only distinction is coercions and any methods available on Box available in the first but not the second? Both examples reference the same object in memory.

So it is consistent. I had been confused by the documentation as to the utility of this distinction between a Box and a reference. To me, I was thinking a Box is a way of saying to create the object on the heap and return a reference. The documentation didn't seem to firmly disavow me of that interpretation.

I was wondering why we needed this first-class type Box when all it did was coerce to a reference any way. But I guess you could (or do?) have methods on a Box that you don't want to available on a reference?

The owner of an object has the ability (and responsebility) to free the memory when an object goes out of scope. In notriddles example the box of cookies is passed to eat, and therefore eat is given the ability (and responsebility) to free the memory. In practice the memory is freed automatically when the object goes out of scope inside eat. Rust checks at compiletime that the memory isn't acccessed after the object is destroyed.

In most rust code any object has an owner that is reponsible for freeing the memory when the object goes out of scope. This ensures that that all memory is eventually freed without the use of any tracing GC and RC. However there are situations where this approach does not suffice. A. These examples include: Cyclic ownership graphs, double linked lists, and containers in which each element has a reference to points to the parent. Therefore you sometimes have to use something like Rc or Arc. AFAICS Manishearth has even implemented a simple tracing GC in rust.

If you are interested in borrowing, then I recommend the following video.

Was this a reply to my last reply to @notriddle? If yes, I don't see how it answered my question unless it is buried in a video.

Perhaps you were just making post in general and not specific to my prior post.

Thanks for the video, maybe I can find time to watch it soon.

I think you are confusing ownership and access permission. The concepts are related but quite orthogonal.

  • Ownership is about all object desctruction.
  • Borrowing is about giving access to an object.

Think of the following analogy:
If I borrow a book from you, you still own the book, but you currently cannot access it. I can access the book, but I don't own it. Because of that, I must not destroy it, but return it to you at some time.

Ownership is transferred moving, not by borrowing.

The main difference between T and Box is that T is stack allocated (or directly inside another object) and Box heap allocated.
With respect to ownership and access permissions, they are roughly equivalent. You can move or borrow both in the same way.
Usually you only need a Box when the size of T is no statically known (trait objects for example).

References give you access to an object, Box additionally means ownership. Of course, with a Box, you also have access to that object (unless it is borrowed), but that's not the main point.

This is not mutually exclusive.
You can read the book, do some practical programming and still have interesting discussions here.
It's just sometimes a bit difficult to understand if your ideas are just coming from a lack of understanding the language or if they are truly innovative. It would also help to keep the focus on the interesting topics instead of explaining the basic concepts.

Thanks for clarifying.

My point is I don't see the utility of redundant concepts that afaics aren't orthogonal.

A Sized, Copy type is stack allocated and always has a copy policy. Afaik, the only way to access it, is with its type. Are borrowed references allowed on Copy types?

A Box type is heap allocation and by default has a move policy. We can alter the default policy with the & annotation. In no case did the access to a Box stop being via a reference.

The borrowing policy is orthogonal, but the memory and access are not.

That is one of those inconsistencies in the design that stood out like a wart for me. But perhaps I am not understanding something? Which is why I am asking.

No offense intended in tone, but I don't see why having an issue with clean concepts of design constitutes my discussion being labelled as "basic concepts". To some extent I understand that I haven't toed for the official line and just accepted it as is and morphed my thinking to fit what the compiler allows. Instead I have taken a more free thinking route of conceptualizing it as how I am conceptualizing it would be the most clean conceptually and discussed it from that perspective. Perhaps that can be construed as wasting the time of others. Or it can be construed as valuable to have people question everything at this early stage and think about everything from first concepts rather than just falling in line.

Any way, I think this tangent of discussion is almost complete and we did discuss some more interesting issues upthread I think (well at least for me they were). Apologies if anyone feels I am wasting their time. Hopefully my new thread will be of value.

No, a Copy type has copy policy, not move.

  • If you pass a non-Copy type by value, it is moved.
  • If you pass a Copy type by value, it is copied. Copy means the same as POD (plain old data) in C++

I don't understand that sentence.

Sure, the concepts are orthogonal.

Move vs copy policy is only relevant for pass-by-value. borrowing is pass-by-reference.

The T value in Box<T> is always accessed via indirection, but not strictly by &-reference in the Rust sense.
You can borrow it to get a &T reference though.

The difference is subtle:

  • In the case of Box<T>, the lifetime of the object is determined by the lifetime of the box.
  • In the case of &T, the maximum lifetime of the reference is determined by the lifetime of the object.

References can never keep an object alive. A reference can never refer to an object that is not owned by something else. I that sense they are fundamentally different to e.g. references in Java.

Apologies it was a typo.

I didn't know this. Well I should have known because the examples in the "The Book" are using Copy types, but when I first read the "References and Borrowing" chapter, I didn't realized a Vec was a Copy type.

I suppose I didn't think it made sense because why take a reference if a primitive type (e.g. u32) and Vec as a Copy type is still perplexing to me.

As I understand a Vec is a Copy type. So when I take a copy, what enforces that the implementation of Vec doesn't allow the "copy" to mutate the data of the copy it was copied from in such a way that the length field in the copied structure is not invalidated? The header structure of Vec is copied, but the data structure is not. It seems far too complex. But perhaps there is a simpler explanation? (And yes I did watch a very long 1+hour video about this from one fo the Rust devs before arriving at this forum and it seemed to be so complex that he didn't even explain it all in one hour but that was very early in my learning process so perhaps it would make more sense to me by now)

So Box<T> says I own the lifetime of the referenced T. And &T says I borrowed a reference to T.

Afaics, that is not any where in "The Book" documentation (which I had read or at least skimmed):

https://doc.rust-lang.org/book/ownership.html
https://doc.rust-lang.org/book/references-and-borrowing.html
https://doc.rust-lang.org/book/lifetimes.html
https://doc.rust-lang.org/book/box-syntax-and-patterns.html

So I think it is quite unfair to say that I didn't try to learn the basic concepts. That is what the forum is here for to ask questions that aren't covered well in the docs. And to give pointers on how to improve the docs.

I still think borrowing should be the default for Box<T>. So then I don't need to write those & all over the place.

And I still don't understand the utility of the distinction between &Box<T> and &T. It seems we should choose one of those syntax because Box<T> and &T are not orthogonal, but rather opposites. You've told me that Box<T> is a move. And &T is a borrow.

I think the use case for the concept of Rust lifetimes which I can't do without is the tracking a compile-time mutable references and borrowing from them. Not for memory management, but because being able to guarantee that there is no other copy of the reference to the mutable object (thus borrowed mutability would be distinct type from owned/unique copy of mutability and of course borrowed immutability would be needed).

This will for example eliminate the requirement for immutability of the collection for those methods that modify the first-class union element type in my proposed complete solution to the Expression Problem.

It will also enable when to know whether closures form pure functions (for convenience and to eliminate unnecessary boilerplate) for my other thread about inversion-of-control of resource management.

Edit: follow-up.

Vec is not a copy type. Pretty much the only types that can have copy semantics are plain numbers, references, enums with no associated data, and zero-sized types like ().

Well I'm very confused now then:

https://doc.rust-lang.org/book/references-and-borrowing.html

fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // do stuff with v1 and v2

    // hand back ownership, and the result of our function
    (v1, v2, 42)
}

I have no clue why that can be written without Box<Vec<i32>> (move) or &Vec<i32> (borrow). What I think I am looking at above is type which must be a Copy type because it is not boxed ???

I find the documentation to be unclear.

Box is not required to move data, even if the data is non-Copy (like Vec is). Frankly, the only reasons using Box ever makes sense are:

  • if you're defining a recursive data type like a singly-linked list (because Rust does not allow data types with infinite size).

  • if the data type you're defining is so huge that adding an extra level of indirection is less expensive than copying it around to move it.

Box and Vec are both container objects. They both allocate memory on the heap, and they both have move semantics. So the example in he book could just as well have been written like this.

fn foo(v: Box<i32>) -> (Box<i32>) {
    v
}

Here is another example to illustrate the difference between move semantics and borrowing. Perhaps that is simpler (link: Rust Playground )

// Note that the argument v is borrowed, because v is a reference 
fn foo(v: &Box<i32>) {
    println!("Printing from foo {}",*v);
}

// Note that the argument v is move because Box has move semantics
fn bar(v: Box<i32>) {
    println!("Printing from bar{}",*v);
}

fn main() {
    let x = Box::new(42);
    foo(&x);
    println!("Printing from main {}",x);
    bar(x);
    
    // The following line would fail
    // The problem is that the ownership of x has been transferred to bar
    // Therefore we can no longer access x from the main function
    //
    // println!("Printing from main {}",x);
}

EDIT: FIxed error. Thanks GolDDRanks

You meant to say: move semantics.

A type with copy semantics leaves the original memory location intact when used, whereas a type with move semantics invalidates the original memory location on use, ensuring the value doesn't get cloned by accident.

1 Like

The difference between move and copy types is not boxed vs non-boxed but trivially copiable vs non-trivially copiable.
For example, Vec cannot be trivially copied, and thus cannot be copy. "Copy" types are always POD (plain old data), i.e. they cannot have destructors (Drop).
Every non-copy type uses move semantics. If a Vec is passed by value, it is moved (like move constructors in C++). That means, that the internal buffer of the Vec is used for the new object. It's actually a simple memcpy. But this means, the original object is left in an indeterminate state and must not be used anymore. And this is statically checked.

Box is also a move type, it guarantees unique ownership of the object it points to. It's like a Vec with exactly one element.

EDIT:
The example that you cited is indeed confusing. Both Vecs are moved to the function and then returned (again moved). But the function doesn't have to return the same Vecs that it received, it can return any two Vecs that it likes.
It's just a function taking two Vecs by value and returning two Vecs by value.

That phrase could be misinterpreted to mean that Box does not always have move semantics. I understand you mean that, "It is not required for a type T to be Box<T> in order for the type to have move semantics. Any non-Copy type (like Vec is) will have move semantics.".

I had remembered that I had looked up the source code for Vec in the past when I was reading "The Book" Rust documentation.

The confusion for me was that I was thinking that if the any portion of the data type (i.e. a struct) could be stored on the stack with a fixed Sized, then it was either assumed or could explicitly inherit some Copy type. I was thinking that only by putting a Box<T> around T would then T become a non-Copy type.

Now I understand that what determines a Copy type, is whether all of the data can be stored on the stack and that there are not complications with copying the data.

So now it is making sense to me that the only way to have copy semantics with a non-Copy type is to first clone it, then move it.

I still don't know how the compiler determines which are the Copy types though. I don't know the exact rules and how it is inferred or declared.

Unless I simply missed the correct section of the "The Book", perhaps this is an area that needs improved explanation in the documentation. Or perhaps it is just due to the fact that my brain is often in between semi-conscious and waking dream-like brain fog (uggh you don't want to know!). Battling an illness that has neurological impacts...

I had always tested very, very high on reading comprehension, but I do have a problem now with my coherence boundary between sleep and awake being fuzzy, so sometimes it all gets jumbled.


P.S. I hate to make excuses but there is fact at play which is I am still trying to work my way out of a strange undiagnosed chronic auto-immune (gut dysbiosis?) illness which (manifests with symptoms similar to M.S.) often makes me suffer CFS (chronic fatigue syndrome) symptoms which are I guess best understood as "forehead on the keyboard after 36 hours w/o sleep", yet in my case I can have that symptom even upon awaking. Sometimes I will have my very sharp and crisp energy, but other times I will be slogging through mud mentally and sometimes it hits me right in the middle of some complex discussion so I will drop the ball temporarily. After 4 years of acute decline following 6 years of insidious decline, finally it appears I am getting some improvement to this ailment, but yet there will still be periods where I am pushing too hard where I should really just be sleeping all day, next day, and next day. So sometimes I could go to the computer for a few hours fighting my sleep because I already slept 8 hours, but really I should on those occasions stay in the bed for an additional 12 hours. Any way, sorry to mention this, but I just want you to know it isn't intentional, nor do I expect you to treat me any differently because of it. Hopefully this illness will be entirely cured soon. I am on a new treatment regimen which appears to be helping.

Copy is a trait, so it is implemented for those types by one of two means: either an impl declaration or a deriving attribute (i.e. impl Copy for Foo { } or #[derive(Copy)]). Copy has Clone as a supertrait, so Clone must also be implemented for the type. In my experience, the most common way to implement Copy for a type is giving it the attribute #[derive(Copy, Clone)].

Note that some built-in types are Copy but not Clone, specifically arrays with more than 32 members, but this is a minor implementation detail that will hopefully be resolved eventually (e.g. by const parameterization).