Newbie missing something on value returns

I was going to try to adjust my code to get rid of some Rc<> usage. And I realized that either what I was going to do wouldn't work, or I don't understand why it would work. Two pieces:

  1. When returning a structured value from a method (e.g. new), the structure gets built on the stack. Does it then get copied into the callers stack space when it is returned?
  2. Suppose struct A { ... field: B } where B is itself a struct. and I have a method A
pub fn fill_b(mut &self, fillval: B) {
    self.field = fillval;
}

does that copy fillval? Do I need to .clone() fillval explicitly? The goal is for the instance of A to be the owner of the instance of B. And the instance of A will live longer than the method that is calling fill_b. WHich suggests I will need lifetime specifiers? At which point I become uncertain as to whether iced will complain as this is in the iced update method. Which makes sure my A instance is mutable. But doesn't generally like lifetime specifiers, I think.
Yours,
Joel

The exact semantics depend on what type fillval is. If the type implements Copy, it will be copied, otherwise it will be moved.

You only need lifetime specifiers if the types involved are borrows. Otherwise you should always use owned types in structs.

Depends on compiler optimizations. Semantically, the value is constructed in a “place” within the function (if not an explicitly named variable, then something like a temporary variable inserted by the compiler) and then “moved” into the return place indicated by the caller.

(Rust’s semantics technically have no notion of “stack” vs “heap”, but in practice that means that the value, ignoring optimizations, would be constructed within the function’s stack frame and moved into the caller’s stack frame.)

And what does the word “move” mean? Well, ignoring optimizations, a “move” is a memcpy where the compiler considers the moved-from place to become uninitialized. Moves are very similar to copies, aside from whether the compiler lets you continue using the original copy of the value.

So, ignoring optimizations, Rust inserts quite a lot of temporary places and moves. The compiler can then optimize them out if it can prove that doing so would not change the observable behavior of your program.

1 Like

Abstractly, by the rules of the Rust language, the value is moved from the expression that built it to the temporary location for the function’s return value, and then, usually, moved again to the final location the caller wants it.

Concretely, the optimizer will try to avoid actually copying bytes as much as it is allowed to. (But that doesn’t change whether you’re allowed to borrow the value while it’s being returned — you aren’t, because there is a move.)

In Rust, a “copy” is not a different operation than a “move”. A copy is a move, but without the restriction that you cannot use a value that was moved elsewhere. In fill_b, the variable fillval is used exactly once, so the move restriction is not relevant. In this code, fillval is moved.

If you added a second use of fillval to the code, then if B: Copy, fillval would be copied, and if not B: Copy, then the code would be erroneous.

No, because you have ownership of the B and you are not using fillval more than once.

In almost all cases, if the code compiles without a .clone(), then you don’t need a .clone().

You have achieved that goal.

No, there is no borrowing here (except for the temporary &mut self to make changes to A), so you do not need to write any lifetimes.

Yes, fields work that way.

Rust lifetimes -- those 'a things -- are poorly named. They do not represent the liveness of values or when something will be destructed. They represent the duration of a borrow. You're not borrowing and don't need lifetimes here.

Try not to conflate Rust lifetimes and value liveness. (Unfortunately, lots of learning materials do.)

(You also can't make values last longer via lifetime annotations. The borrow checker is a pass-or-fail analysis that makes sure your code meets any constraints described by the annotations. It does not change the what the code does, like where values are destructed. The lifetime annotations do not prescribe behaviour.)

1 Like

Thanks for all the explanations. Yes, I was conflating the lifetime of a thing with the lifetime of borrowing the thing. I now get that was yet another confusion of mine in learning to think like rust. And it sounds like a lot of the excess copies I was worrying about are theoretically there, but in practice the optimizer takes care of it, since it will indeed be provable that it can just be built in the right place.
Yours,
Joel

Moves and copies are usually cheap, and clones are sometimes expensive. There are exceptions to both, but a good starting place is to not worry about copies and moves, and maybe rethink things if you have a ton of clones.

(I wouldn't worry too hard about this type of optimization while you're still in the learning phase generally.)

Sorry to keep asking. (And even if I could post all the code, as soon as I try an answer I would be back asking again I'm sure.) I very much appreciate the rolerance. I presume part of the problem is that I am missing something conceptual in HashMaps.
It is possible that this is also cause by the fact that I am still using Refcell in the container for this (I am trying to remove the Rc<> from person before removing the Rc and RefCell from the holding struct.) Given the HashMap<usize, Person>, I want a simple method to borrow a reference to the person at a give Hash key. I thought:

    pub fn get_person<'a>(self: Rc<Self>, id: usize) -> Option<&'a Person> {
        self.people.borrow().get(&id)
    }

would do the job. (The Borrow is because the Hashmap is in a RefCell. I am hoping that is not the cause of the error. I will get rid of that once I get the first part sorted out.)
The compiler complains that

    |         returns a value referencing data owned by the current function
    |         temporary value created her

Thanks,
Joel

This has nothing to do with HashMap; it is the tradeoff of using RefCell.

Suippose that some type Foo owns a value of another type Bar. Then, Foo choosing to put Bar in a RefCell allows you to mutate the Bar only given &Foo, but at the cost that it is impossible to write a “projection” function fn(&'a Foo) -> &'a Bar, because you can only produce such a reference if the Bar won’t be mutated, and without RefCell, having &'a Foo constitutes a proof that Bar won’t be mutated until 'a ends, but using RefCell means that &'a Foo isn't a proof that the Bar won’t be mutated, at all.

(Also, you can’t produce such a reference anyway using self: Rc<Self>, since that is a transfer of part-ownership, not a borrow, but that’s a less fundamental issue since you could write &'a self or self: &'a Rc<Self> instead.)

Solutions include:

  • Don’t use RefCell at all; design your application to not need it. This is usually best when it is possible. This usually requires removing Rc too.

  • Use Ref::map() to return a Ref to the Person (that keeps the RefCell runtime-borrowed as long as it exists), instead of an & reference. However, this is awkward to do if the lookup might fail.

  • Make the get_person function take whatever type is directly inside the RefCell, so it has that to borrow.

  • Put each Person in its own Rc and return a cloned Rc instead of an & reference.

Essentially, the problem is that you have to do this either all at once (remove all the Rc and RefCell), or the opposite order (remove the Rc and RefCell from your container before you replace Rc<Person> with Person).

Thanks. I was afraid it was going to be "remove all the Rc, and RefCell in on go." Oh well. I made the mistake, so now I put in the work to fix it :slight_smile: Yours,
Joel