Skip to content

Latest commit

Β 

History

History
executable file
Β·
314 lines (233 loc) Β· 11.4 KB

File metadata and controls

executable file
Β·
314 lines (233 loc) Β· 11.4 KB
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 here

This is because of Ownership, which is used to achieve Rust's memory safety.

Ownership

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 Copy trait):
      • The value is duplicated.
      • Both variables become independent owners of their own data and remain accessible.
    • Move (default behavior/ if not implemented Copy trait):
      • Ownership transfers to the new variable.
      • The original variable is invalidated and can no longer be used.

πŸ’‘ In Rust, every assignment is technically a bitwise move (a memcpy/ byte-for-byte copy). The difference is that if a type implements the Copy trait, the compiler simply doesn't invalidate the original variable afterward.

Copy Types

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 std traits for std types.
  • 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`

Implementations in std

  • Primitive types, arrays of Copy types, tuples of Copy types, raw pointers *const T/ *mut T, shared references/ &T

    marker_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, P are Copy types, then Option<T>, Result<T, E>, Pin<P>, Poll<T>

  • IpAddr, Ipv4Addr, Ipv6Addr, Ipv6MulticastScope, SocketAddr, SocketAddrV4, SocketAddrV6, SystemTime, Duration, Instant, PhantomData, and more

More Examples

#![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]
}

Move Types

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.

Move Types in std

  • Heap allocated collections like String, Vec<T>, VecDeque<T>, LinkedList<T>, HashMap<K, V>, HashSet<T>, BTreeMap<K, V>, BTreeSet<T>, BinaryHeap<T>
  • &mut T and most iterators
  • When T, E, P are non-Copy types, then Option<T>, Result<T, E>, Pin<P>, Poll<T>
  • PathBuf, File, TcpStream, TcpListener, JoinHandle, Child processes, channel Sender/Receiver, Mutex<T>, RwLock<T>
  • Smart Pointers like Box<T>, Rc<T>, Arc<T>, Pin<P>
  • Cell<T>, RefCell<T>

More Examples

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<T>

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

πŸ‘¨β€πŸ« Before going to the next...

  • 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.
  • A smart pointer is a type that wraps a pointer and implements the Deref and Drop traits. This provides additional functionality such as heap management (Box<T>), shared ownership (Rc<T>, Arc<T>), etc.