| title | Ownership |
|---|---|
| slug | ownership |
We discussed in the Derive Traits, the usage of Copy marker trait and .clone() with the below code.
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 0, y: 1 };
let b = a;
println!("{a:?}, {b:?}");
}If we try to remove Copy derive on Point and run, we will get the following error while compiling.
error[E0382]: borrow of moved value: `a`
--> src/main.rs:11:16
|
8 | let a = Point { x: 0, y: 1 };
| - move occurs because `a` has type `Point`, which does not implement the `Copy` trait
9 | let b = a;
| - value moved hereThis is because of Ownership, which is used to achieve Rust's memory safety.
In Rust,
- Each piece of data has a single owner, and data is only scoped to its owner (π― if it is not borrowed).
- This means that when the owner of the data goes out of scope, the bound resource will be dropped/ released from memory.
- There can only be one owner at a time. Assigning a variable to another variable or passing it to a function (if not passed by referencing) triggers one of the following behaviors:
- Copy (for the types that implement the
Copytrait):- The value is duplicated.
- Both variables become independent owners of their own data and remain accessible.
- Move (default behavior/ if not implemented
Copytrait):- Ownership transfers to the new variable.
- The original variable is invalidated and can no longer be used.
- Copy (for the types that implement the
π‘ In Rust, every assignment is technically a bitwise move (a memcpy/ byte-for-byte copy). The difference is that if a type implements the
Copytrait, the compiler simply doesn't invalidate the original variable afterward.
Rust Standard Library has implemented the Copy trait inside std for primitive types and most simpler types mainly,
- Due to the orphan rule, we are not allowed to implement
stdtraits forstdtypes. - To avoid needing to call the
.clone()method each time when assigning data to a new variable or passing it to a function.
Also as we discussed, the Copy marker trait can be added to custom types via a derive macro. However, the Rust compiler will fail to compile if the type contains any non-Copy members.
#[derive(Debug, Clone, Copy)]
struct Person {
name: String, // π‘ String is not a Copy type. It's a Move type.
}
fn main() {
let a = Person { name: "Steve".to_string() };
dbg!(a);
}error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:1:24
|
1 | #[derive(Debug, Clone, Copy)]
| ^^^^
2 | struct Person {
3 | name: String,
| ------------ this field does not implement `Copy`-
Primitive types, arrays of
Copytypes, tuples ofCopytypes, raw pointers*const T/*mut T, shared references/&Tmarker_impls! { #[stable(feature = "rust1", since = "1.0.0")] Copy for usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128, f16, f32, f64, f128, bool, char, {T: PointeeSized} *const T, {T: PointeeSized} *mut T, } #[stable(feature = "copy_clone_array_lib", since = "1.58.0")] impl<T: Copy, const N: usize> Copy for [T; N] {} /// Shared references can be copied, but mutable references *cannot*! #[stable(feature = "rust1", since = "1.0.0")] impl<T: PointeeSized> Copy for &T {}
-
When
T,E,PareCopytypes, thenOption<T>,Result<T, E>,Pin<P>,Poll<T> -
IpAddr,Ipv4Addr,Ipv6Addr,Ipv6MulticastScope,SocketAddr,SocketAddrV4,SocketAddrV6,SystemTime,Duration,Instant,PhantomData, and more
#![allow(unused)]
fn main() {
let a = [1, 2, 3]; // π‘ Array of Copy types
let b = a;
let c = (true, 'C', 32, 1.2); // π‘ Tuple of Copy types
let d = c;
let e = Color { red: 255, green: 0, blue: 0 }; // π‘ Type with Copy typed members + Copy marker
let f = e;
println!("{:?} {:?}", a, b); // [1, 2, 3] [1, 2, 3]
println!("{:?} {:?}", c, d); // (true, 'C', 32, 1.2) (true, 'C', 32, 1.2)
println!("{:?} {:?}", e, f); // Color { red: 255, green: 0, blue: 0 } Color { red: 255, green: 0, blue: 0 }
}
#[derive(Debug, Clone, Copy)]
struct Color {
red: u8,
green: u8,
blue: u8,
}fn main() {
let x: [i32; 3];
{
let a = [1, 2, 3];
x = a;
println!("{:?}", a); // [1, 2, 3]
// `a` is dropped here, but `x` holds its own independent copy
}
println!("{:?}", x); // [1, 2, 3]
}By default, data in Rust follows move semantics unless the type implements the Copy marker trait.
Since Rust prohibits implementing Copy for types containing non-Copy members, we must manually call the .clone() method to duplicate the data.
Types like String, Vec<T>, and Box<T> in Rust store the unsized actual data on the heap and small, fixed-size metadata on the stack. When we assign these types to a new variable, only the stack metadata is copied, the original variable is invalidated, ownership of the heap data is transferred to the new variable, and only the new owner is responsible for freeing the heap memory.
However, when we call .clone() on these types, the heap data is duplicated (deep copy), unless types like Rc<T> and Arc<T>, which are specifically designed to be cloned efficiently by incrementing a reference count on the heap rather than duplicating the actual data in heap.
- Heap allocated collections like
String,Vec<T>,VecDeque<T>,LinkedList<T>,HashMap<K, V>,HashSet<T>,BTreeMap<K, V>,BTreeSet<T>,BinaryHeap<T> &mut Tand most iterators- When
T,E,Pare non-Copytypes, thenOption<T>,Result<T, E>,Pin<P>,Poll<T> PathBuf,File,TcpStream,TcpListener,JoinHandle,Childprocesses, channelSender/Receiver,Mutex<T>,RwLock<T>- Smart Pointers like
Box<T>,Rc<T>,Arc<T>,Pin<P> Cell<T>,RefCell<T>
fn main() {
let a = String::from("Hello");
let b = a.clone();
let c = vec![1, 2, 3];
let d = c.clone();
let e: [String; 2] = ["Steve".to_owned(), "Jony".to_owned()];
let f = e.clone();
let g = (128, "Steve".to_string());
let h = g.clone();
println!("{:?} {:?}", a, b); // "Hello" "Hello"
println!("{:?} {:?}", c, d); // [1, 2, 3] [1, 2, 3]
println!("{:?} {:?}", e, f); // ["Steve", "Jony"] ["Steve", "Jony"]
println!("{:?} {:?}", g, h); // (128, "Steve") (128, "Steve")
}Iterators created with into_iter() consume the collection and move ownership of its data into the iterator. In contrast, iter() and iter_mut() are used to iterate over shared and mutable references of the collection, without moving it.
fn main() {
let a = vec![1, 2, 3];
// βοΈ By the way, we can use references without cloning which we'll discuss under borrowing
for x in a.clone() { // π¨βπ« Try remove `.clone()`
println!("{x}");
}
println!("{}", a.len());
}
// π‘`for x in a` uses `into_iter()`
// π―`for x in &a` uses `iter()` and `for x in &mut a` uses `iter_mut()`Structs with non-Copy members are always moved. We need to explicitly call .clone() when we want to duplicate the data.
#![allow(unused)]
#[derive(Debug, Clone)]
struct Person {
name: String,
company_name: String,
age: f32,
}
fn main() {
let steve = Person{ name: "Steve Jobs".to_owned(), company_name: "Apple".to_owned(), age: 56.0 };
let steve_copy = steve.clone();
// βοΈ copy fields values from another instance, steve_copy. steve_copy.company_name moves to wozniak
let wozniak = Person { name: "Steve Wozniak".to_owned(), .. steve_copy };
println!("{} {}" , steve_copy.name, steve_copy.age); // We can't access steve_copy.company_name here
dbg!(steve, wozniak);
}fn main() {
let x: String;
{
let a = String::from("Hello!");
x = a; // Ownership is transferred to `x` and `a` is invalidated
// At the end of this block, `a` goes out of scope.
// However, no heap data is freed because ownership was moved to `x`
}
println!("{x}"); // "Hello!"
}Box stores its value on the heap and provides ownership for the allocation.
fn main() {
let a = Box::new(5); // Data on the heap + Smart pointer Box<i32> on the stack
let b = a.clone(); // Deep copy: data on heap + smart pointer on the stack & new owner
let c = a; // Move ownership of original Box to c
println!("{b} {c}");
}π― Conceptually, Box::new(T) creates T on the stack before moving it to the heap, the compiler (via LLVM) often performs Stack-to-Heap "Move" Optimization in --release mode. This avoids a double move (Stack to Heap) by constructing the value directly in its final heap location rather than copying it from the stack. The concept is called "placement new" in some languages.
π― Conceptually, Box::new(String::from("Hello!")) involves two separate heap allocations/ double allocation. Original "Hello!" on one heap allocation and String structure on second heap allocation. If we use a Box<str>, it requires only one heap allocation.
use std::mem::size_of_val;
fn main() {
let a = Box::new(String::from("Hello!"));
let b: Box<str> = "Hello!".into(); // π‘let b = Box::from("abc"); also works
println!("Sizes of Box<String>");
println!("1. Stack size (thin ptr): {} bytes", size_of_val(&a)); // 8 bytes
println!("2. Heap size (String struct): {} bytes", size_of_val(&*a)); // 24 bytes
println!("3. Heap size (actual data): {} bytes", size_of_val(&**a)); // 6 bytes
println!("\nSizes of Box<str>");
println!("1. Stack size (fat ptr): {} bytes", size_of_val(&b)); // 16 bytes
println!("2. Heap size (actual data): {} bytes", size_of_val(&*b)); // 6 bytes
} STACK HEAP (Alloc 1) HEAP (Alloc 2)
[ Box ptr ] -> [ String Struct ] -> [ "Hello!" ]
(Thin Ptr) (ptr, len, cap)
STACK HEAP (Alloc 1)
[ Box ptr, len ] -> [ "Hello!" ]
(Fat Ptr)
π‘ A thin pointer is just a memory address: 8 bytes on 64-bit systems (1 machine word)
π‘ String: 24 bytes (3 words: ptr, len, cap)
π‘ Actualy data: N bytes on heap-
A pointer is a memory address that refers to data stored elsewhere in memory (Heap, stack, or in the binary's static memory).
- If the pointer points to a Sized type, only the memory address is stored (
&i32,Box<String>). We call this a thin pointer. - If the pointer points to an Unsized type (DST/ Dynamically Sized Type), it contains the memory address and some metadata such as a length (
&str,&[T],Box<str>) or a vtable pointer (Box<dyn Trait>). We call this a fat pointer.
- If the pointer points to a Sized type, only the memory address is stored (
-
A smart pointer is a type that wraps a pointer and implements the
DerefandDroptraits. This provides additional functionality such as heap management (Box<T>), shared ownership (Rc<T>,Arc<T>), etc.