How `move` works in Rust

It might sound like dumb questions...
I couldn't find good articles about move in Rust although found many articles about move and ownership relationship. My questions are that can I move copyable variables, and does move shallow copy the variable (object), (prob not but )deep copy or something else since Rust is pass by value lang?

example 1

#[derive (Clone, Copy, Debug)]
struct Bar {
    x: i32,
}

fn callBar(x: Bar) { 
  // do something
}

fn main() {
  let bar = Bar { x: 3}; 
  callBar(bar);  // with Copy trait, it is copied instead of move. can I move bar instead of copy here?
}

example 2

struct Bar {
    x: i32,
}

fn callBar(x: Bar) { 
  // do something
}

fn main() {
  let bar = Bar { x: 3}; 
  callBar(bar);  // Does bar variable still point to the same memory like C++ in variable x in callBar func?
}
2 Likes

Rust moves do include a shallow copy indeed (unless that step is optimized out by the compiler).

A move in Rust is essentially the combination of shallow copy of the value and a static check by the compiler that the old value cannot be used anymore after the move.[1] This also motivates why we don’t need any way to “move bar instead of copy”, because a copy just does less (still shallow copy, but skip the part where the compiler restricts access to the moved-out-of variable).

In your example 2, indeed x inside of callBar will have a different memory address than bar outside. At least without any further optimization by the compiler. I’m not quite sure I understood what aspect of “like C++” you were asking about though, in case that’s still unclear, perhaps you’ll need to explain in more detail what the question there was.


  1. This latter part also includes the (mostly static) mechanism that any Drop code is not executed for values that were moved; and things like assigning a new value can still be permitted for a variable after the old value was moved. ↩︎

1 Like

Thank you for answering the questions quickly and enlightening me about Rust move.
What I meant about C++ move is that C++ move is used to avoid reconstructing data, especially for big data on heap memory like vector, from scratch instead of the deep copy. My question was that does Rust move work like C++? Sounds like the answer is yes (shallow copy) based on your response.


I wasn't sure about this part. Can we still move variables with copy trait?
For example, in the example1, what if the function signature looks like callBar(x: Vec<BigData>) which x would take the value that contains millions of data (big sized data), but vector is implemented with Copy trait, can we still move the value instead of copy since copying value in this case would take long time?

In cpp

void copyBar(std::vector<Bar>&& x) {}  // move variable

void copyBar(std::vector<Bar> x) {} // copy variable. inefficient if variable contains big size of data

in Rust

let x = 3; // this is copyable
let y = std::move(x) // its a pseudocode. wondering if Rust has explicit `move` func for copyable variable.
// x ownership is gone even though x is copyable

Let’s see… the most important piece of information to keep in mind here, and something where Rust differs from C++, is that neither Copy nor moves are in any way customizable with user code. They always do exactly that single memcpy of size_of::<T>() many bytes, the shallow size of the struct (or enum, or array) in question. For large array types (array means, the type “[T; N]”) or structs containing such types, or extreme cases of other deeply nested structs, this can sometimes be nontrivial overhead (and those kind of types also tend to be able to eventually overflow your stack anyways, when you work with them). But in most cases, std::mem::size_of::<Type>() will be rather small for most types, so it’s cheap to copy or move.

As a consequence, it’s also never more expensive to copy instead of to move.

For Vec<Bar>, that type is a (growable) vector on the heap. The size_of::<Vec<Bar>>() on the other hand is always just 3 * size_of::<usize>(). So that’s how much a move costs. What about a copy? Vec<T> is a type that owns memory, and needs a custom destructor. Those types are prevented from implementing Copy in the first place. So implicit copying of a Vec is impossible in Rust. (Yay, no accidental implicit and unwanted deep or semi-deep copies!)

If you want to copy a Vec, you’d use the Clone trait instead, through which many types gain a .clone() method. That’s expensive then for a large Vec<Bar>, but to do it to foo: Vec<Bar>, you’ll have to write foo.clone() explicitly.[1]


So, no, Rust has nothing like std::move in C++, but that’s because it doesn’t need it. A move and a (implicit) copy in Rust are each built-in operations that are (generally) cheap, and not user-definable; and for a type that supports (implicit) copying, a move would never actually be cheaper; their run-time behavior is essentially identical.[2]


The only setting in which you do effectively really move a value of some Copy-able type anyways is in generic settings. E.g. you write a function

fn foo<T>(x: T) {
    let y = x; // moves `x`, even in cases when `foo` is called with type `T` whose values copyable
}

this does do a move, even when called as foo(3) so with T = i32.


  1. Well… not always you directly; of course another function/method you call can also call it explicitly, it should then of course document the performance implications that might have. ↩︎

  2. A minor difference I’m aware of is in terms of optimization potential. IIRC there are certain optimization strategies, when passing arguments by-value, where in the machine code, a larger non-Copy types can be passed by-reference, whereas a Copy-supporting types needs to keep a defensive copy. ↩︎

7 Likes

One more thing to note is that Rust doesn't have a language-level notion of a "deep clone". Some types like Rc<_> and Arc<_> are cheap to .clone(), but don't duplicate the data of their parameterized type (because shared ownership is their point).

You can only be sure of a deep clone of you know ask the types involved and their Clone implementations (e.g. not in a generic context).

Additionally, some types that could implement Copy only implement Clone for a variety of reasons.

2 Likes

Perhaps partially off-topic (though also not really, since it does even include a section about move and copy, too) — with some C++ knowledge as a foundation, I can especially recommend giving this video a watch :wink:

1 Like

I can recommend an excellent article C++ Move Semantics Considered Harmful (Rust is better) from The Coded Message blog. It goes in depth about the differences between C++ and Rust move semantics and argues why Rust's destructive moves are better than C++'s non-destructive moves.

4 Likes

Vec is most definitely not Copy. It never was, and never will be, because it can't be. That would be wrong, because it manages a heap buffer, so blindly bitwise duplicating the ownership would lead to a double-free bug.

That's why you can only move (or actively clone) a Vec. And yes, as explained above, moving a value is a bitwise copy, so in the case of a Vec, it only copies the (pointer, capacity, length) triple. It doesn't copy the contents of the heap buffer.

1 Like

Thanks for the great explanation. I'll also watch the video too.

Thanks. I'll check it out.

Yep, I understand that Vec is not Copy. I just wanted to assume its Copy to compare with C++, but that leads to the confusion. Should have used other proper example.

Copy in Rust is TriviallyCopyable in C++.
Clone in Rust is CopyConstructible in C++.
Move in Rust is a memcpy and does not have an equivalence in C++ because it
has no destructive move.
MoveConstructible in C++ does not exist in Rust because it's not needed. (You just move things by default). At best you can treat std::mem::take (in Rust) as a std::move analog but it's not strictly equivalent and it serve different purpose.

8 Likes

Thanks!

As I have understood: every variable has at lease some data on the stack. For i32, f64, boolean and at lot others, that's all there is to this data. A copy actually copies this data from one location of the stack to another. All of these data types implement the Copy-Trait. That includes an array (those with a fixed length)

If you have a data structure that consists only of types implementing the Copy-Trait, this structure can also implement this trait and will be copied when used for example as a parameter in a function call. If you combine a lot of arrays in one struct, a rather large data chunk can evolve, but this is probably a rare case.

In contrast, some data types have something on the stack and additionally some other data somewhere else, most of the time on the heap. Examples are String or Vec or Box.

These types and all structures having at least on of those cannot implement the Copy trait. They are moved. Move means that the stack 'part' of the data is copied as before, and the rest is retained. Therefore, to avoid multiple access to one chuck of data, the compiler forbids accessing the data after the move. So, as stated before, a copy and a move in machine code is the same operation.

A special case is Pointers / References (&T, &str …). They have some data on the stack as well as other data somewhere else (obviously). However, these types do implement the Copy-trait because the borrow checker forbids multiple writing access via References.

3 Likes

That's a good explanation. "The book" also describes this.

I just have a minor comment, and it isn't related to moving/copying per se, so feel free to ignore it.

It is possible to have an object, for example a String, that has nothing on the stack at all, if for example the String is in a field in a structure, and that structure is allocated on the heap. Or if the String is in a static variable or a thread-local variable.

One way to think of it is that objects are referenced, directly or indirectly, by at least one "root" stack variable or global/static/thread-local variable. If they weren't, they would be inaccessible to your program.

No. You mean "inline". If you put a primitive integer in a dynamically-allocated container (be it a Box or a Vec), then it won't be on the stack.

This has nothing to do with writeability at all. If writeability were the issue, then impl Clone for Cell<T> would be unsound, as cells allow shared mutability.

The issue is ownership, which is the right to destroy. It is completely orthogonal to mutability. If String only exposed an immutable, read-only API, it would still be wrong to drop two String instances that point to the same heap buffer.

3 Likes

It could potentially be cheaper to move in some cases because moving allows the compiler to avoid a defensive memcpy and keep the variable in the same memory.

It doesn't seem like the compiler takes advantage of that optimization opportunity though.

I did read about certain defensive copy situations before, and also noted so in the footnote on the original comment. (Admitted not the section you quoted, but further down on the sentence “their run-time behavior is essentially identical”.) I can see if I can find a code example where it shows in ASM.

Edit: No, I couldn’t find any code that does it, either.

1 Like

First: None of questions are dumb. People who call questions dumb are dumb.

Second: All of these are compiler rules. Compiler acts as like the ownership moved or not. And compiler checks that is a struct implemented the Copy trait or not. And generated assembly code for it.

1 Like

Rust doesn't give LLVM enough information to do that. I don't think LLVM even has an attribute that enables that. I tried add a few of LLVM's parameter attributes, but none of them cause the memcpy to be removed.

But more importantly the semantics of move operands (in MIR) aren't even fully decided, but that probably way too technical, other than saying it is definitely not a "dumb question".