Barrow in Rust

Sometimes we want to pass a value to a function without transferring ownership. This is where borrowing comes in.

Borrowing in Rust is the mechanism that allows you to give temporary access to a resource without transferring its ownership. It is a way of lending a resource to a function or a code block for a limited time. This helps prevent ownership-related problems, such as data races or resource leaks, by enforcing strict rules on how data can be accessed and modified. Barrowing is achieved by passing a reference to the variable (& var_name) rather than passing the variable/value itself to the function

There are two types of borrowing in Rust: immutable and mutable borrowing.

A reference is a way to borrow a value without taking ownership of it.

There are two types of references in Rust:

  1. Immutable references or Shared barrow or reference (&T) :

-> The borrowed resource cannot be modified.

-> You can have one or more &T values at any given time, or

2. Mutable references (&mut T).

-> Allows the borrower to modify the resource.

-> You can have exactly one &mut T value.

Example of immutable borrowing:

#[derive(Debug)]
struct Point(i32, i32);

fn add(p1: &Point, p2: &Point) -> Point {
    Point(p1.0 + p2.0, p1.1 + p2.1)
}

fn main() {
    let p1 = Point(1, 2);
    let p2 = Point(10, 20);
    let p3 = add(&p1, &p2);
    println!("&p3.0: {:p}", &p3.0);
    println!("{:?} + {:?} = {:?}",p1,p2,p3);
}
/*
In debug mode :
&p.0: 0x7ffd01e27280
&p3.0: 0x7ffd01e27348
Point(1, 2) + Point(10, 20) = Point(11, 22)

============================================
In Release mode :

cargo run --release

   |
   |

&p.0: 0x7ffff3fc3b60
&p3.0: 0x7ffff3fc3b60
Point(1, 2) + Point(10, 20) = Point(11, 22)
amit@DESKTOP-9LTOFUP:~/OmPracticeRust/jaiShreeRam$

*/        

Please note there is change in the address between &p.0 and &p3.0

Debug mode(both are different )

&p.0: 0x7ffd01e27280

&p3.0: 0x7ffd01e27348

Release mode(both are same):

&p.0: 0x7ffff3fc3b60

&p3.0: 0x7ffff3fc3b60

The reason the addresses change in the "DEBUG" optimization level and stay the same in the "RELEASE" setting is because of the different optimization levels. In "DEBUG" mode, the compiler inserts additional checks and code to assist with debugging and tracing, such as bounds checking and additional debugging symbols. These extra checks and symbols can cause the memory layout of the struct to be different, leading to different memory addresses. In contrast, in "RELEASE" mode, the compiler optimizes the code to produce faster and smaller binaries, removing many of the additional checks and symbols, which leads to a more consistent memory layout and hence the same memory addresses.

Example of mutable borrowing:

fn add_numbers(v: &mut Vec<i32>) { 
    v.push(42); 
} 

fn main() { 
    let mut my_vec = Vec::new(); 
    add_numbers(&mut my_vec); 
    println!("{:?}", my_vec); 
}         

In the above example, add_numbers() borrows my_vec mutably using a mutable reference (&mut). This means that add_numbers() can modify my_vec, adding a number to it.

Borrowing can also be done using slices, which allow you to borrow a portion of an array or a vector:

fn sum_slice(s: &[i32]) -> i32 {
    let mut sum = 0;
    for i in s {
        sum += i;
    }
    sum
}

fn main() {
    let my_vec = vec![1, 2, 3, 4, 5];
    let my_slice = &my_vec[1..3];
    let result = sum_slice(my_slice);
    println!("{}", result);
}        

In this example, my_slice borrows a slice of my_vec that contains elements at indices 1 and 2. sum_slice() then borrows my_slice immutably and calculates the sum of its elements.

Borrowing is a powerful concept in Rust that allows you to write safe and efficient code by enforcing strict ownership and borrowing rule

Immutable references allow the borrower to read the value, while mutable references allow both reading and writing.

fn main() { 
    let mut x = 5; 

    // borrow `x` immutably 
    let y = &x; 

    // borrow `x` mutably (this is not allowed while `y` is in scope) 
    // let z = &mut x; 
 
    // print `x` and `y` 
    println!("x = {}, y = {}", x, y); 
} 
/*
Op => 

amit@DESKTOP-9LTOFUP:~/OmPracticeRust$ ./OwnerShip
x = 5, y = 5
 
*/        

In the above example, we create a mutable variable x and then borrow it immutably with the reference y. We can read the value of x through y, but we cannot modify x while y is in scope. If we try to borrow x mutably with z while y is still in scope, the code will not compile.

Borrowing is a mechanism in Rust that allows multiple references to a value without transferring ownership. A borrowed reference has a lifetime that is determined by the scope in which it is borrowed. When the scope ends, the borrowed reference is automatically dropped.


Below are some example code where borrowing a variable both mutably and immutably at the same time would result in a compile error:

Example 1:

fn main() {
    let mut v = vec![1, 2, 3]; 
    let first = &v[0]; 
    v.push(4);
    println!("{}",first);
} 
/*
Compilation error :
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> main.rs:6:5
  |
5 |     let first = &v[0]; 
  |                  - immutable borrow occurs here
6 |     v.push(4);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |     println!("{}",first);
  |                   ----- immutable borrow later used here


error: aborting due to previous error


For more information about this error, try `rustc --explain E0502`. 
*/        

Explanation: In this example, v is borrowed immutably by the first variable and mutably by the v.push(4) statement at the same time. But this also alone will not generate the compilation error, since immutable reference is not used anywhere.

But when I try to print , I am accessing both mutable(v) and immutable reference(first) in the program.

Note: If I comment above println! line, then no compile error happens.

This would result in a compile error because having both mutable and immutable references to the same data at the same time violates Rust's borrowing rules.

Example 2:

fn main() {
    let mut x = 5; 
    let y = &x; 
    let z = &mut x; 
    println!("{} {}",x,z);
} 

/*
error[E0502]: cannot borrow `x` as immutable because it is also borrowed as mutable
 --> main.rs:7:22
  |
6 |     let z = &mut x; 
  |             ------ mutable borrow occurs here
7 |     println!("{} {}",x,z);
  |                      ^ - mutable borrow later used here
  |                      |
  |                      immutable borrow occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`. 
*/        

Explanation: In this example, x is borrowed immutably by the y variable and mutably by the z variable at the same time.

When I try to print the values , I am accessing the mutable immutable referance at the same time. This would result in a compile error because having both mutable and immutable references to the same data and access at the same time violates Rust's borrowing rules.

Note: If I comment above println! line, then no compile error happens.

Example 3:

fn main() {
	let mut v = vec![1, 2, 3]; 
	let first = &v[0]; 
	v.clear(); 
	println!("{}",first);
}
/*

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> main.rs:6:2
  |
5 |     let first = &v[0]; 
  |                  - immutable borrow occurs here
6 |     v.clear(); 
  |     ^^^^^^^^^ mutable borrow occurs here
7 |     println!("{}",first);
  |                   ----- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`. 
*/        

Explanation: In this example, v is borrowed immutably by the first variable and mutably by the v.clear() statement at the same time. This would result in a compile error because having both mutable and immutable references to the same data at the same time violates Rust's borrowing rules.

Note: If I comment above println! line, then no compile error happens.


Rust compiler is capable of return value optimization (RVO).

Return Value Optimization (RVO) is a compiler optimization technique that can eliminate unnecessary copies of objects when returning values from functions. In Rust, this means that the compiler can optimize away the copy/move of a return value by constructing it directly in the memory location of the caller.

This optimization can improve performance by reducing the overhead of copying/moving objects, especially for large or complex objects. In addition, it can help to reduce memory usage, which can be particularly important in resource-constrained environments.

However, RVO is not always possible or desirable, especially when dealing with objects that have side effects or require unique ownership. In such cases, it may be necessary to use explicit move or clone semantics to ensure correct behavior.

Rust compiler has Borrow checker:

Rust's borrow checker is a compiler feature that helps prevent memory-related bugs like null pointer dereferences, dangling pointers, and data races by enforcing ownership and borrowing rules.

As explained earlier in Rust, every value has an owner and can be borrowed by other parts of the code. When a value is borrowed, the owner gives temporary permission to use the value, but still retains the responsibility of freeing the memory when the value is no longer needed.

The borrow checker ensures that there is no conflict between ownership and borrowing by enforcing the following rules:

  1. Each value has a unique owner: A value can have only one owner at any given time. When the owner goes out of scope, the value is dropped and its memory is freed.
  2. Borrowing: A value can be borrowed either as a mutable reference or an immutable reference. Mutable references are exclusive, meaning that only one mutable reference can exist at any given time. Immutable references can coexist with other immutable references, but not with mutable references.
  3. Lifetimes: Every reference has a lifetime, which determines how long the reference can be used. A lifetime is a way of specifying how long a value exists, and it is used to ensure that borrowed values don't outlive their owners.

The borrow checker analyzes the code at compile time and checks if these rules are being followed. If the rules are violated, the compiler will generate an error, preventing the program from compiling.

For example, let's consider the following code:

fn main() { 
    let mut x = 5; 
    let y = &mut x; 
    let z = &mut x; 
    println!("{}", y); 
}         

The above code will not compile because x is being mutably borrowed twice, violating the ownership and borrowing rules. The borrow checker detects this and generates a compile-time error.

The borrow checker is a powerful feature that allows Rust to provide safe memory management without requiring runtime checks or garbage collection. However, it can also be challenging to work with, especially when dealing with complex data structures and lifetimes. Understanding how the borrow checker works is essential for writing safe and efficient Rust code.


Lifetimes

Lifetimes are another important concept in Rust's memory management system. Lifetimes are a way to specify the scope of a reference, and they ensure that borrowed data remains valid for the duration of the reference.

In Rust, every value and reference have a lifetime associated with it, which determines the scope of the borrowed data. This is important because Rust needs to ensure that values are not used after they have been deallocated, which can cause memory safety issues such as use-after-free bugs. Lifetimes are denoted by a single quote character or apostrophes ('), followed by a name that represents the lifetime. A reference with a lifetime indicates that it borrows a value for a specific amount of time, and it cannot outlive the value it borrows.

Read more details in : Lifetime in Rust | LinkedIn

Things to remember :

  • Only one part of a program owns a variable.
  • By default, when you pass a variable to a function, you move it. You can always move it back!If you want to copy it, you can use clone()If you want to retain ownership, you can borrow with &
  • Every borrow has a lifetime, even if you can't see it.
  • The compiler will prevent you from making use-after-move, use-after-free, dangling pointer errors. That's whole classes of CVEs from C++ eliminated, without losing performance by using a garbage collector.


In summary, borrowing and lifetimes are essential concepts in Rust that allow for safe and efficient memory management. Rust enforces strict rules to ensure that a value is not used after it has been moved or mutated, preventing common programming errors like null pointer dereferencing, data races, and memory leaks. By using the borrow checker, Rust can catch these errors at compile time rather than runtime, making it a more reliable and secure language.

Borrowing involves borrowing references to data rather than copying them, which can save memory and processing time. Borrowing can be done either immutably or mutably, with only one mutable reference allowed at any given time. Rust also supports lifetime annotations that help the compiler ensure that references are not used after their original data is no longer valid.

While lifetimes can be explicit or elided, the Rust compiler always infers them automatically. This ensures that code is efficient and correct, without requiring the programmer to manually manage the lifetimes of all variables and references. By using these concepts, Rust provides memory safety and performance without requiring garbage collection or other runtime overhead.

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics