Skip to Content
RustBooksThe Rust BookCh 4. Ownership

Ch 4. Ownership

Recap: stack v.s. heap

The Stack

  • Structure: Works as last in, first out (LIFO). Think of a stack of plates—you add or remove from the top.
  • Storage: Stores data with a known, fixed size at compile time (e.g., integers, booleans).
  • Efficiency: Pushing data to the stack is fast because there’s no searching for space; data is placed at the top.
  • Access Speed: Data on the stack is quickly accessed because it’s stored in a predictable order and is close in memory.

The Heap

  • Structure: Less organized; memory is allocated as needed.
  • Storage: Used for data with an unknown or changing size at runtime. The memory allocator finds a space big enough and returns a pointer to the location.
  • Efficiency: Slower to allocate because the allocator must search for space, and access requires following a pointer.
  • Access Speed: Accessing heap data is slower compared to stack data due to pointer indirection and scattered memory locations.

Key Differences

  • Stack: Fixed size, fast to push/pop, data is close together.
  • Heap: Variable size, slower to allocate and access, data is scattered, but the pointer can be stored on the stack.

Function Call and Memory

  • Function Calls: Parameters and local variables are pushed onto the stack. When the function ends, these values are popped off.
  • Pointers: Heap data is accessed via pointers. The pointers are stored on the stack.

Purpose of Ownership in Rust

  • Ownership primarily manages heap data:
    • Tracks which parts of the code are using what data.
    • Minimizes duplicates and cleans up unused heap data to prevent memory leaks.

4.1 What Is Ownership?

  • Ownership is a key feature in Rust that manages memory through a set of rules enforced by the compiler.
  • Unlike other languages that use garbage collection or manual memory management, Rust’s ownership system provides a safe and efficient way to handle memory without runtime overhead.

Ownership Rules

There are three main rules of ownership in Rust:

  1. Each value in Rust has a single owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (freed from memory).

Variable Scope

  • Scope refers to the range in a program where a variable is valid and can be used.
  • A variable is created (comes into scope) when declared and is dropped (goes out of scope) when its enclosing block ends.

Example of Scope

{ // s is not valid here, it’s not yet declared let s = "hello"; // s is valid from this point // do stuff with s } // s goes out of scope and is no longer valid
  • In this example, the string literal "hello" is valid inside the block where it’s declared. Once the block ends, the variable s is no longer accessible.

Example: the String Type

Rust’s String type allows for more flexible and dynamic memory allocation than string literals, which are immutable and have a fixed size.

  • Mutable: Unlike string literals, String can be modified.
  • Heap Allocation: String stores its data on the heap, making it flexible for storing text of unknown or variable size at runtime.
let mut s = String::from("hello"); s.push_str(", world!"); // Modifies the String println!("{s}"); // Prints "hello, world!"

On the other hand, String Literal (&str)

  • Immutable and stored directly in the program’s binary.
  • Known size at compile-time, so no need for heap allocation.

String Type

  • Allocates memory on the heap to handle data that is mutable or whose size is unknown at compile-time.
  • When a String is created, memory is requested (allocated) from the system.
  • When the String goes out of scope, Rust automatically deallocates the memory using its ownership model.
  • When a variable goes out of scope, Rust automatically calls a function called drop to free the memory.
{ let s = String::from("hello"); // s is valid here } // s goes out of scope, and memory is freed automatically

This automatic memory management, similar to C++‘s RAII (Resource Acquisition Is Initialization), prevents memory leaks and ensures memory safety.

Copying Simple Values

For simple data types like integers, Rust makes a copy when assigning one variable to another:

let x = 5; let y = x; // x is copied into y

Both x and y hold the value 5 because integers are stored on the stack, which is fast and safe to copy.

Moving with String

When assigning a String to another variable, Rust moves the data rather than copying it:

let s1 = String::from("hello"); let s2 = s1; // s1 is moved into s2
  • Move: The ownership of the String is transferred from s1 to s2. After the move, s1 is no longer valid and cannot be used.

  • This prevents both s1 and s2 from pointing to the same heap memory, which could cause issues like double freeing the memory.

  • Invalidated value example:

let s1 = String::from("hello"); let s2 = s1; println!("{s1}"); // Error: s1 is no longer valid after the move
  • Rust ensures memory safety by preventing the use of s1 after its data has been moved to s2.

Deep vs. Shallow Copying

  • Rust avoids deep copies (copying heap data) automatically to prevent performance overhead.
  • The process of moving data from s1 to s2 is inexpensive because only the stack data (pointer, length, and capacity) is copied, not the heap data.

Variables and Data Interaction with clone

In Rust, when you want to deeply copy heap data (not just the pointer and metadata), you can use the clone method. This method explicitly duplicates the underlying heap data as well, rather than simply transferring ownership of a reference.

let s1 = String::from("hello"); let s2 = s1.clone(); // Clones the heap data println!("s1 = {s1}, s2 = {s2}"); // Both s1 and s2 are valid
  • The clone method copies all the data, including what is stored on the heap. This is different from a shallow copy (like a move), which only copies the stack data (pointers and metadata).
  • Calling clone may be expensive in terms of performance, as it duplicates the data in memory.

Stack-Only Data and the Copy Trait

For simple types that are stored on the stack and have a fixed size, Rust uses the Copy trait to allow lightweight copying without moving ownership. When a type implements Copy, assigning it to another variable creates a copy rather than a move.

let x = 5; // i32 implements the Copy trait let y = x; // x is copied, not moved println!("x = {x}, y = {y}"); // Both x and y are valid
  • No clone is needed here because i32 implements the Copy trait.
  • Types that implement Copy include simple, stack-only types like integers, Booleans, and characters.

The Copy Trait

Rust’s Copy trait allows for trivial copying of values. Types that implement Copy are automatically copied when assigned to another variable or passed to a function. If a type or any of its parts implements the Drop trait (used for custom memory cleanup), it cannot implement Copy.

Types that implement Copy include:

  • Integer types (e.g., u32, i32)
  • Boolean (bool)
  • Floating-point types (f64, f32)
  • Character (char)
  • Tuples, if all elements implement Copy (e.g., (i32, i32) but not (i32, String))

Ownership and Functions

When passing variables to functions, Rust’s ownership rules determine whether the value is moved or copied, similar to how assignments work.

fn main() { let s = String::from("hello"); takes_ownership(s); // s is moved, and no longer valid here let x = 5; makes_copy(x); // x implements Copy, so it can still be used after the function call } fn takes_ownership(some_string: String) { println!("{some_string}"); } // some_string goes out of scope and is dropped fn makes_copy(some_integer: i32) { println!("{some_integer}"); } // some_integer goes out of scope, but nothing special happens
  • takes_ownership takes ownership of some_string, and after the function call, s is no longer valid.
  • makes_copy works with an i32, which implements Copy, so x remains valid after the function call.

Return Values and Ownership Transfer

Functions can return values, transferring ownership back to the calling function. This allows a function to give back ownership after performing some operations.

fn gives_ownership() -> String { let some_string = String::from("yours"); some_string // Ownership is moved to the caller } fn takes_and_gives_back(a_string: String) -> String { a_string // Ownership is moved back to the caller } fn main() { let s1 = gives_ownership(); // s1 gets ownership of the returned String let s2 = String::from("hello"); let s3 = takes_and_gives_back(s2); // s2 is moved into the function, and s3 takes ownership }
  • Ownership of some_string is moved from gives_ownership to s1.
  • The function takes_and_gives_back takes ownership of s2, and then moves it back to the caller in s3.

Returning Multiple Values

Rust allows returning multiple values using tuples, which can simplify the process of transferring ownership and returning additional data.

fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) // Return the String and its length as a tuple } fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); // s1 is moved, and length is returned println!("The length of '{s2}' is {len}."); }

4.2 References and Borrowing

In Rust, references allow you to refer to a value without taking ownership of it. This mechanism enables borrowing data in a function without transferring ownership.

fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // Borrow s1 by reference println!("The length of '{s1}' is {len}."); // s1 is still valid } fn calculate_length(s: &String) -> usize { // Function borrows a reference s.len() // Returns the length of the String without taking ownership }
  • &s1 creates a reference to the String s1 but does not take ownership.
  • In the function signature, &String indicates that the function is borrowing the reference to the String.
  • The reference allows s1 to be used after calling the function because ownership wasn’t transferred.

Important Concepts

  • References: Created with the & symbol, they let you access data without owning it.
  • Borrowing: Refers to the process of using references, allowing access to data while leaving the ownership with the original variable.
  • Dereferencing: Using the * operator to access the value that the reference points to (discussed in more detail later).

Mutability of References

By default, references in Rust are immutable, meaning you cannot modify the borrowed value through an immutable reference. If you need to modify the data, you must use a mutable reference.

fn main() { let s = String::from("hello"); change(&s); // Immutable borrow, attempt to modify will fail } fn change(some_string: &String) { some_string.push_str(", world"); // Error: cannot modify an immutable reference }

To modify a borrowed value, you need to explicitly declare a mutable reference.

fn main() { let mut s = String::from("hello"); // The original variable must be mutable change(&mut s); // Pass a mutable reference println!("{s}"); // Output: hello, world } fn change(some_string: &mut String) { some_string.push_str(", world"); // Modify the borrowed String }
  • The change function takes a mutable reference &mut String and modifies it.
  • You can only modify the borrowed value if both the original value and the reference are declared as mutable.

Rules of References and Borrowing

Rust enforces strict rules on references to prevent data races at compile time:

  1. At any given time, you can have either one mutable reference or any number of immutable references to a particular piece of data.

  2. References must always be valid.

    fn main() { let s1 = String::from("hello"); let r1 = &s1; // Immutable reference 1 let r2 = &s1; // Immutable reference 2 println!("r1: {r1}, r2: {r2}"); // Both references are valid }
  3. You cannot have both mutable and immutable references at the same time.

fn main() { let mut s = String::from("hello"); let r1 = &s; // Immutable reference let r2 = &mut s; // Mutable reference (Error: cannot have both) println!("{r1}, {r2}"); // Compile-time error }
  • Rust does not allow both mutable and immutable references to coexist because it could lead to data races.

Dangling References (Prevention)

Rust’s borrowing system prevents dangling references, which occur when a reference points to memory that has been freed.

fn dangle() -> &String { let s = String::from("hello"); &s // Error: returning a reference to a variable that will be dropped }
  • In this example, s will be dropped when the function ends, leaving a dangling reference. Rust prevents this by refusing to compile the code.

4.3 The Slice Type

  • Slices allow you to reference a contiguous sequence of elements in a collection (like an array or string) without taking ownership.
  • Since slices are references, they are lightweight and efficient for reading parts of collections.

The problem of not using slice

Consider a scenario where you need to write a function that takes a string of words separated by spaces and returns the first word. If there is no space, the entire string should be returned.

Initially, without using slices, you might think of returning the index of the first word’s end. Here’s the function that does that by returning the index:

fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); // Convert string to byte array for (i, &item) in bytes.iter().enumerate() { if item == b' ' { // Check if the byte is a space return i; // Return the index of the space } } s.len() // If no space, return the length of the string }

This approach finds the index of the first space in the string. However, the returned index is not tied to the string itself, making it error-prone if the string changes after the function call.

fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word is 5 (index of first space) s.clear(); // The string is now empty, but word is still 5! // word is now invalid because the string has changed println!("The first word ends at index: {word}"); }

This code compiles, but word becomes invalid after s.clear() because the index refers to a now-empty string, leading to potential bugs.

Solution: Using String Slices

  • Instead of returning an index, we can return a slice of the string.
  • A string slice is a reference to a portion of the string, tying the result directly to the string’s data. This ensures that if the string changes, we won’t use invalid references.
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); // Convert to byte array for (i, &item) in bytes.iter().enumerate() { if item == b' ' { // Check for space return &s[0..i]; // Return the slice from start to space } } &s[..] // If no space, return the whole string slice }

This version returns a string slice &str rather than an index. The slice directly references the original string, preventing invalid memory access.

fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word is a slice: "hello" s.clear(); // Clears the string // This would now fail to compile, ensuring safety println!("The first word is: {word}"); }

Compiler Error for Invalid Slices

If you attempt to modify the string after creating a slice, Rust will prevent the program from compiling:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

This error occurs because Rust disallows mutable and immutable references to the same data at the same time, preventing potential bugs.

Slicing Syntax

Here’s how to use slices with different ranges in Rust:

  • Full slice:

    let slice = &s[..]; // Slice the entire string
  • From start to index:

    let slice = &s[..5]; // Slice from the start to index 5
  • From index to end:

    let slice = &s[3..]; // Slice from index 3 to the end
  • Middle slice:

    let slice = &s[3..8]; // Slice from index 3 to index 8

Benefits of Slices

Using slices provides several advantages:

  • Tied to the original data: Slices are references to the original data, ensuring they are only valid as long as the original data exists.
  • Safety: Rust’s borrow checker prevents invalid or dangling references by enforcing strict borrowing rules at compile time.
  • Efficiency: Slices are references, so no data is copied, making them efficient.

String Literals are actually Slices

let s = "Hello, world!";
  • The type of s is &str, which is a string slice pointing to the data embedded in the program’s binary.
  • This explains why string literals are immutable: they are references (&str), and references to data can’t be modified directly unless they are mutable, which string literals are not.

String Slices as Parameters

Knowing that string literals are slices, we can further generalize functions like first_word. The earlier version of first_word had a signature that accepted a &String:

fn first_word(s: &String) -> &str {

However, a better version accepts a &str parameter instead:

fn first_word(s: &str) -> &str {

This more flexible version allows the function to handle both &String and &str values seamlessly. When you pass a String to the function, Rust automatically converts it to a slice. This process, called deref coercion, simplifies function usage by reducing the need for explicit conversion.

fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] }
fn main() { let my_string = String::from("hello world"); // Works on slices of `String` let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // Works on references to `String` let word = first_word(&my_string); let my_string_literal = "hello world"; // Works on slices of string literals let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // Works directly on string literals, as they are already `&str` let word = first_word(my_string_literal); }

General Slices

Beyond string slices, Rust provides a more general slice type that works with collections like arrays. You can create a slice of an array in a similar way to slicing a string:

let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]);

This array slice has the type &[i32], which works similarly to string slices. You can use slices with other types of collections, like vectors

Last updated on