Copy Types and Move Types in Rust

Move type error in Rust
A vector is a move type, so ‘x’ can’t be called after assignment to ‘y’.

In Rust, the value of a variable can be either copied or moved when assigned to a new variable.

Whether the value is moved or copied depends on the type. In this context, primitive types are called copy types while non-primitive types are called move types.

The topic of copy types and move types was introduced in Ownership but this article covers it more extensively, specifically with regards to memory management on the stack and the heap.

One of the most important aspects of copy and move types is that ownership is never shared in any case. When reassigned, copy types create a whole new copy that maintains ownership over its’ own copy of the data. In contrast, move types transfer ownership of the data to the new variable.

When one variable is copied to another, the original variable is referred to as the assignee variable and the new variable is referred to as the assigned variable.

Copy Types

Copy types are data types that are copied when reassigned. The new variable has ownership over its’ own copy of the data, and the original variable maintains ownership of the original data.

Copy types are stored directly on the stack. The size is known at compile time and they can be quickly and cheaply copied. Copy types include:

Why are primitive types copied?
Primitives are stored directly on the stack so it is fast and cheap to make a copy of them.

For example, an integer is a primitive type and therefore also a copy type.

The following will compile properly because the value of x (42) is copied to y. When x is called by the println! macro, we don’t encounter an error because x still has ownership of its’ own copy.

let x = 42;
let y = x;

println!("The value of x is: {}", x);
println!("The value of y is: {}", y);

Standard Output:

The value of x is: 42
The value of y is: 42

Copy Types and the Stack

It’s helpful to visualize the stack in order to understand how copy types work. Let’s look at the previous example:

let x = 42;
let y = x;

When x is declared, the value of ’42’ is assigned to it and stored on the stack. The stack frame would look something like this:

AddressNameValue
0x42

Then when a new variable y is declared and x is assigned to be its’ value, the memory allocator creates a copy of x and sets the name equal to y:

AddressNameValue
1y42
0x42

Both x and y have ownership of their own copy of the value ’42’. That’s why we can still call x without getting an error.

Move Types

Non-primitive types are stored partially on the heap and partially on the stack. The main data is stored on the heap. The stack stores the information needed to retrieve the data from the heap like the pointer and length. It would be costly to create a new copy of the data stored on the heap so non-primitives are moved rather than copied.

When a move occurs, the original variable loses ownership. This means that we can no longer access the original variable.

Why are non-primitive types moved?
Non-primitives are stored primarily on the heap, with other data like a pointer stored on the stack. Creating a whole new copy of both would be a costly operation in terms of speed and memory, so non-primitives are moved rather than copied.

For example, a vector is a non-primitive type and therefore gets moved instead of copied. In the following example, we can access the vector by calling ‘y’:

let x = vec![1,2,3];    // x is the owner of the vector
let y = x;                  // y is the new owner of the vector

println!("{:?}", y);  // we can retrieve the vector using y

Standard Output:

[1,2,3]

But if we try to retrieve the vector using ‘x’, we will encounter an error because ‘x’ no longer owns the vector:

let x = vec![1,2,3];    // x is the owner of the vector
let y = x;                  // y is the new owner of the vector
println!("{:?}", x);  // error because x no longer owns the vector

Standard Error:

let x = vec![1,2,3];  
      - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
let y = x;             
             - value moved here
println!("{:?}", x); 
                          ^ value borrowed here after move

This can be better understood from the perspective of the heap.

Move Types and the Heap

Move types are non-primitives and are stored in the heap rather than the stack. However, a pointer to the address (memory location) of the data as well as other information are stored on the stack:

Move types live on the heap.

When a move type variable is reassigned, the data on the heap remains unchanged. However two things happen:

  1. The data on the stack gets copied from the original variable to the new variable.
  2. The original variable is invalidated and loses ownership of the data.

In a sense, copy and move operations are similar in that the action really occurs on the stack. The difference is that when a move occurs, the original variable is invalidated, loses ownership, and therefore can no longer be called.

Shallow vs. Deep Copy in Rust

The topic of copy and move types is related to the idea of shallow copying and deep copying.

In other programming languages, a shallow copy is when a copy of the original object is stored and only a reference is copied. This is similar to a move in Rust, except that ownership is transferred and the original variable is invalidated. As a result, there is no such thing as a true shallow copy in Rust.

A deep copy occurs when the entire object is copied and both the original and new copies are stored. This is the same as the copy operation in Rust.