Will assignment (`=`) always be a deep copy if the type derives/implements Copy&Clone?

For example,

let val = 42;
... // use val
let new_val = val;
... // val will not be used anymore

In this example, it is safe to transfer the ownership of val to new_val. Will the Rust compiler first check if the ownership transfer is possible before doing a deep copy? Or will the compiler always to a deep copy no matter val will be used any more?

What about more complex data structures, e.g., val is a struct that contains many types that all derive/implement Copy&Clone? Will the compiling rules regarding ownership transferring and deep copy be different?

assignment will never do a deep copy. Copying a Copy type will always only be able to do a shallow copy. This means: Only data on the stack is copied; the amount of data being copied is the same as the amount of data being moved when moving a value of that type, so copying is no overhead over moving.

Deep copies in Rust, i.e. a copy where some data behind a pointer is also copied, new heap-data is allocated, etc, can only happen explicitly; usually by calling .clone() on a type that implements Clone (but not Copy).


If the data structure can implement Copy that means that it contains its data immediately, without indirection. This means that moving a value of that type will also always do a copy to the new location of all its data under the hood. Even though fields of a struct, or fields of a fields of a struct are involved, this would still be considered a shallow copy.

If moving a struct that needs to be moved around a lot is considered too much overhead, because the struct is very big, then adding a Box can be a useful way to reduce overhead of moving. It’s a trade-off because it will introduce overhead of dereferencing the Box on every access, and allocation+deallocation when it’s created or destroyed. Also, adding a Box means the struct will no longer be able to implement Copy; a (derived) Clone implementation would then perform a deep copy, but it needs to be called explicitly.


For more details on the differences (and similarities) of moving and copying in Rust, also see this recent post of mine.

8 Likes

Thanks very much for the help! I'm still a little confused with the copy rules regarding struct. Considering this case:

struct S {
  i: u32,
  f: f32,
  v: Vec<u32>
}

fn foo() {
  let mut sv = Vec::<S>::new();
  ... // update sv with several `S`
  let mut ss =  HashSet::<S>::new();
  for s in sv {
    ss.insert(s);
  }
}

Inside the loop, will the whole body of every S in the vector be copied (I understand that S.v will not go through a deep copy of the vector) to ss? Or since the contents of sv are already on the heap, will only the pointer to each S in sv be copied to ss?

There will be happening (perhaps multiple steps of) shallow copying of the S values because they’re moved. Feel free to take a look at the whole thread from which my comment I linked above was. Even thought he S values are on the heap when inside the Vec<S>, and they’re on the heap when inside the HashSet<S>, their location on the heap will need to change when moved from the vec to the hashset: Both Vecs and HashSets use larger heap allocations to contiguously[1] store all their elements in memory. Not every single element has its own heap allocation. This avoids a level of indirection, and it means that pushing a value into a Vec that was previously not on the heap, will not need to create a new heap allocation (unless the Vec was full, in which case it grows, which does involve a new allocation); so that’s some performance benefits.

In terms of how much data is copied around in your for loop: In each iteration, one value of type S is moved from the Vec’s iterator to the stack variable s and then into the HashSet. Each level of non-inlined function call either returning the S when getting it from the iterator, or receiving the S in the .insert call, will add another move. So there’s at least 2 moves of a value of type S per iteration, possibly more unless everything is inlined.

Each move of an S involves a shallow copy of the data, i.e. it copies:

  • the u32 in the i field (4 bytes)
  • the f32 in the f field (4 bytes)
  • the shallow data of the v field, which consists of
    • a pointer to the Vec<u32> data (8 bytes on a 64-bit machine)
    • the length of the Vec<u32> (8 bytes on a 64-bit machine)
    • the capacity of the Vec<u32> (8 bytes on a 64-bit machine)

Essentially, moving a S will copy as much as std::mem::size_of::<S>() many bytes:

struct S {
    i: u32,
    f: f32,
    v: Vec<u32>
}

fn main() {
    dbg!(std::mem::size_of::<S>());
}
[src/main.rs:8] std::mem::size_of::<S>() = 32

Rust Playground


(I’m ignoring in this discussion that putting something with a f32 field into a HashSet won’t work very well, unless you’re writing a custom Eq and Hash implementation.)


  1. well… in the latter case, as contiguous as it gets for a HashSet, with typically a few empty slots between elements ↩︎

1 Like

The answer to this question depends on what you mean by deep copying.

However, even without getting into arbitrary definitions, a couple of things are unconditionally true:

  1. An assignment is a move (of the RHS value to the LHS place).
  2. For a Copy type, moving is exactly the same as copying.
  3. For a Copy type, the behavior of Clone should also be the same as copying/moving. This is true for all automatically #[derive]'d implementations of Clone. [1]
  4. Moving (or Copying) a value moves/copies around exactly the data that is found in the value; no more, no less. If a copiable type contains references (or raw pointers), then the references/pointers themselves will be duplicated, not their underlying data. This is because references/raw pointers don't imply ownership.
  5. A type for which the deep/shallow copying distinction at all matters cannot be Copy. [2]
  6. A non-Copy type can still be Clone, however, in which case it can be expected to perform a deep Clone (i.e. it does more than a naïve byte-level copying: it actively follows pointers to allocations and Clones them, too), depending on its semantics. Usually, collections behave like this, but there are types for which Clone semantically means something else (e.g. Rc).
  7. Moving a value of such a type still, of course, only moves around the data immediately inside the value, not any external allocations it is pointing to. (This is the whole point of Drop types not being Copy – they cannot be duplicated by blindly copying them byte-by-byte, because they would then result in duplicated, semantically owning, pointers to the same allocation, resulting in a double-free when both copies are dropped.)

  1. Unfortunately, this can be violated by explicitly writing a malicious Clone impl by hand. Needless to say, that's unreasonable to do and is heavily frowned upon. ↩︎

  2. Technically, raw pointers are Copy, and so you can make a type containing raw pointers Copy too. However, the use of raw pointers usually means that you are implementing some sort of collection, and so the type will have a destructor, i.e. a Drop impl, which will prevent it from being Copy. ↩︎

1 Like

While true for many non-Copy types like Box, Vec, HashMap, etc, this if false for types like Rc and Arc and types using Arc such as for example mpsc::Sender, which don't perform a "deep copy" (though e. g. the Sender does some fancy stuff when first cloned for switching from an optimized single-producer mode to a multiple-producer mode). Or for fancy things like persistent data types like Vector in im - Rust. Or types that don't implement Copy to only to avoid confusion when using mutating methods, such as Range in std::ops - Rust or std::slice::Iter, or interior mutability primitives like Cell.

1 Like

That's a good point, I clarified that item.

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.