How does Rust move stack variables that are not Copyable?
Asked Answered
H

1

23

There is a great example of Rust's move semantics documented here: Rust Move Semantics on the Rust By Example website.

I have a basic understanding of both cases demonstrated. The first being how a primitive can have a new alias and the original can still be used because the end result is a copy seeing as i32 utilizes the Copy trait. This makes good sense to me.

Additionally, for many good reasons the second example makes sense in terms of having multiple aliases that refer to an i32 on the heap. Rust enforces ownership rules and therefore the original alias cannot be used now that a new binding has been created. This helps prevent data-races, double frees, etc.

But it would seem there is a third case which is not talked about. How does Rust implement moves of stack allocated structs that do not implement the Copy trait? This is illustrated with the following code:

#[derive(Debug)]
struct Employee{
    age: i32,
}

fn do_something(m: Employee){
    println!("{:?}", m);
}

fn main() {
    let x = Employee {
        age: 25,
    };

    do_something(x);

    //compiler error below because x has moved
    do_something(x);
}

This I know: In the case above, Rust will allocate the Employee on the stack. The above struct does not implement the Copy trait and therefore will not be copied when assigned to a new alias. This is very confusing to me because if the Employee struct is allocated on the stack and also does not implement the Copy trait where/how does it move? Does it physically get moved to do_something()'s stack frame?

Any help is appreciated in explaining this conundrum.

Hussite answered 26/3, 2016 at 1:46 Comment(3)
Would you mind simplifying your example? Would be great to make the Employee struct less complex and at least to remove the lifetime. struct Employee { age: i32 } would be enough, for example.Peipus
@LukasKalbertodt - yes I have simplified the example.Hussite
link has changed: doc.rust-lang.org/rust-by-example/scope/move.htmlNankeen
P
17

Does it physically get moved to do_something()'s stack frame?

Yes. Non-Copy types are physically moved exactly like Copy types are: with a memcpy. You already understood that primitive Copy-types are copied into the new location (new stack frame for example) byte-by-byte.

Now consider this implementation of Box:

struct Box<T> {
    ptr: *const T,
}

When you have

let b = Box::new(27i32);
do_something(b);    // `b` is moved into `do_something`

then an i32 is allocated on the heap and the Box saves the raw pointer to that heap allocated memory. Note that the Box directly (the raw pointer inside) is directly on the stack, not on the heap! Just the i32 is on the heap.

When the Box is moved, it is memcpyed, as I just said. This means that the stack contents are copied (!!)... thus just the pointer is copied byte-by-byte. There isn't a second version of the i32!

There is no difference between Copy and non-Copy types when it comes to moving physically. The only difference is that the compiler enforces different rules upon those types.

Peipus answered 26/3, 2016 at 2:5 Comment(4)
Minor nit: AIUI, there's no guarantee that the values will actually be moved, but the semantics are such that you have to assume that they are. For example, the compiler is free to rewrite do_something to accept a reference to b in order to prevent copying bits around. The important thing is that the code behaves as if the bits were moved.Linebreeding
So, perhaps the actual internal difference is that, when the function returns, for Copy types there's nothing else to do, since the previous value is still usable, but for Non-Copy types, the stack should be "fixed", because the previous (moved) value would still be hanging there somewhere, right? So when is that memory freed? Is the stack compressed somehow, or that value lingers there forever (until that other func returns)?Unmake
@Unmake I'm not 100% sure I understand your questions. Stack memory is freed when a function returns -- regardless of Copy and not. For types implementing Drop, Rust promises to call drop once at the correct place. In order to do that, Rust sometimes has to store some bits in the stack, remembering whether something was moved. Note that Copy types cannot implement Drop. Finally, I'm not sure what you mean by "fixed".Peipus
Great, thanks @LukasKalbertodt! By "fixed" I mean compressed, de-fragmented, removing the values that were moved by a previous function call. But you answered somehow, that Rust stores additional flags that tell if that memory is usable or not. So that memory lingers there, until this function ends 👍Unmake

© 2022 - 2024 — McMap. All rights reserved.