about
03/15/2023
RustBites - SmartPtrs
Rust Bite - SmartPtrs
Box, Rc, Arc, RefCell, interior mutability,
1. std::boxed::Box<T>
Example: Box<T>
2. std::rc::Rc<T>
Example: Rc<T>
3. std::sync::Arc<T>
Example: Arc<T>
4. std::cell::RefCell<T>
Example: RefCell<T>
5. Interior Mutability
6. Conclusions:
7. Exercises:
- Store a string in the heap, and pass around access to several functions. Why could that be useful?
- The smart pointers Rc and RefCell are often used together as Rc<RefCell<T>>. What is the purpose of this construct?
-
We haven't mentioned weak pointers in these notes. Go to the std::rc::Rc documentation
here and map the
relationships between Rc<T>, RcBox<T: ?Sized>, and Cell<T>.
- Read the comments in that documentation, then summarize in a few words.
- Where are the reference counts kept? Why are there two?
8. Other Interesting Problems
-
Create an immutable array of double precision numbers and initialize each element with the time at which each entry was filled, in microseconds. You will find the std::time library useful. Try doing this with a static array. In order to initialize an immutable collection you will need to use std::cell::RefCell.Solution for Problem #1
Problem #1 fn solution_1() { print!("\n -- solution #1 --"); let t:Duration = Duration::from_micros(0); let time_array : [Duration; 5] = [t; 5]; // not mutable let mut t_init = RefCell::new(time_array); let start = std::time::Instant::now(); for i in 0..5 { let inner = t_init.get_mut(); inner[i] = start.elapsed(); } let time_array = t_init.into_inner(); // still not mutable print!("\n time_array = {:?}", time_array); print!("\n type is {:?}\n",show_type(time_array)); } Output: -- solution #1 -- time_array = [1.3µs, 1.6µs, 1.8µs, 2µs, 2.1µs] type is "[core::time::Duration; 5]" The brief code, above, shows in a simple way, how to initialize a non-mutable array using a RefCell container. It's also interesting to observe the time needed to traverse a small loop. -
Create an array of Strings on the heap, where the size of the array is defined at
run-time. Display the strings, then swap the first and last strings and display the
results. Did you have to copy any characters to swap the two strings?
This is a loaded question: arrays are static, with memory layout defined at compile time. If we broaden our definition of array to "indexable collection of items residing in a contiguous memory block" then we can indeed define the size at run-time using a Vec<T>.Now, we can create an array, of size determined at run-time, by creating a full slice of the Vec<T> So we didn't need the broader definition, but did have to take a circuitous path to provide an answer to the question, as asked.If you try to directly swap elements you will run afoul of Rust's ownership rules - you can't have more than one mutable reference. Instead, look at the Vec::swap function. If you look at its source code - it's simple (and available - just google for rust vec swap and press the [src] button). The same is true for swap for slices. That will allow you to fully answer the question.
Solution for Problem #2
Solution #2 ///////////////////////////////////////////////////////////// // This is an exploration of creating and managing arrays. // Almost always you should prefer to use std::Vec. fn exercise_3() { print!("\n -- solution #2 --"); /*-- create static array in stack and swap --*/ let s1 = "one".to_string(); let s2 = "two".to_string(); let s3 = "three".to_string(); let s4 = "four".to_string(); let s5 = "five".to_string(); let mut stack_array = [ s1.clone(), s2.clone(), s3.clone(), s4.clone(), s5.clone() ]; print!("\n stack_array = {:?}", stack_array); stack_array.swap(0,4); print!("\n stack_array = {:?}", stack_array); /*-- create static array in heap and swap --*/ let mut heap_array = Box::new([s1, s2, s3, s4, s5]); print!("\n heap_array = {:?}", heap_array); heap_array.swap(0,4); print!("\n heap_array = {:?}", heap_array); /*-- create dynamic vector in heap and swap --*/ let n = 5; let mut v = Vec::<String>::new(); for i in 0..n { v.push((i + 1).to_string()); } let mut heap_vec = Box::new(v); print!("\n heap_vec = {:?}", heap_vec); heap_vec.swap(0,4); print!("\n heap_vec = {:?}\n", heap_vec); /*-- create array with run-time size and store in heap --*/ let n = 7; // run-time size, could have been passed in by user let mut vn = Vec::<String>::new(); for i in 0..n { vn.push((2*i).to_string()); } /*-- here's array, created as slice of vec --*/ let arr_n = &mut vn[..]; show_type_value("arr_n", &arr_n); let heap_arr_n = Box::new(arr_n); show_type_value("heap_arr_n", &heap_arr_n); heap_arr_n.swap(0,6); show_type_value("heap_arr_n", &heap_arr_n); /*-- bring array back to stack, consuming src --*/ let arr_back = *heap_arr_n; show_type_value("arr_back", &arr_back); /*-- curious things you can do with arrays --*/ ///////////////////////////////////////////// // Can copy string into array of Strings arr_back[2] = "third element".to_string(); ///////////////////////////////////////////// // This sequence of statements copies chars // to effect swap. Uses clone because // Strings can't be moved out of array. let first: String = arr_back[0].clone(); let last: String = arr_back[6].clone(); arr_back[0] = last; arr_back[6] = first; ///////////////////////////////////////////// // This statment just swaps pointers //------------------------------------------- // arr_back.swap(0,6); show_type_value("swapped arr_back", &arr_back); println!(); } Output: -- solution #2 -- stack_array = ["one", "two", "three", "four", "five"] stack_array = ["five", "two", "three", "four", "one"] heap_array = ["one", "two", "three", "four", "five"] heap_array = ["five", "two", "three", "four", "one"] heap_vec = ["1", "2", "3", "4", "5"] heap_vec = ["5", "2", "3", "4", "1"] arr_n = ["0", "2", "4", "6", "8", "10", "12"] &&mut [alloc::string::String] heap_arr_n = ["0", "2", "4", "6", "8", "10", "12"] &alloc::boxed::Box<&mut [alloc::string::String]> heap_arr_n = ["12", "2", "4", "6", "8", "10", "0"] &alloc::boxed::Box<&mut [alloc::string::String]> arr_back = ["12", "2", "4", "6", "8", "10", "0"] &&mut [alloc::string::String] swapped arr_back = ["0", "2", "third element", "6", "8", "10", "12"] &&mut [alloc::string::String] The solution, above, provides a lot of details about using heap memory, working with vectors and with arrays. You will see handled all of the issues cited in the Exercise 3 statement with comments to help illustrate what the code is trying to do. -
Create a string that is simultaneously shared by two different identifiers. What do you have
to do to modify the string?
Shared mutation is not allowed in safe Rust as that can lead to undefined behavior through unsafe memory access.
But, suppose that the shared mutations are staggered in time so that they don't intereact? Rust's borrow checker can often analyze this case and allow it. But sometimes the checker is just not strong enough to detect this structure and a build fails for safe code. This is exactly the primary use case for RefCell. It defers some borrow checking to run-time. Then if the accesses are all truely staggered, the processing succeeds. The answer I want to show you uses RefCell to defer borrow checking to run-time.Solution for Problem #3
run_time_checking:main.rs ///////////////////////////////////////////////////////////// // run_time_checking::main.rs - demo RefCell // // // // Jim Fawcett, https://JimFawcett.github.io, 08 Jun 2020 // ///////////////////////////////////////////////////////////// /* Demonstrate deferring ownership rule checking to run-time by using RefCell. */ #![allow(dead_code)] use std::cell::RefCell; use std::io::*; fn putline() { println!(); } /*----------------------------------------------- This first function compiles. Borrow checker flow analysis accepts it. It has either immutable or mutable borrow, but not both. Borrow checker analyzed this! */ fn test_checker1(p: bool) { let mut s = String::from("this is a test"); let mut rs: &String = &"rs".to_string(); let mut mrs: &mut String = &mut "mrs".to_string(); if p { rs = &s; print!("\n rs = {:?}", rs); } // immutable borrow goes out of scope else { mrs = &mut s; print!("\n mrs = {:?}", mrs); } // mutable borrow goes out of scope print!("\n mrs = {:?}", mrs); print!("\n rs = {:?}", rs); putline(); } /*----------------------------------------------- Borrow checker flow analysis fails here even if called with p1 != p2. Borrow checker can't tell without analyzing caller - too complicated for compiler analysis. Note: this function is same as test_checker1 except now has two predicates. */ // fn test_checker2(p1: bool, p2: bool) { // let mut s = String::from("this is a test"); // let mut rs: &String = &"rs".to_string(); // let mut mrs: &mut String = &mut "mrs".to_string(); // if p1 { // rs = &s; // } // if p2 { // mrs = &mut s; // } // print!("\n mrs = {:?}", mrs); // print!("\n rs = {:?}", rs); // } /*----------------------------------------------- This function replicates functionality of test_checker2. It uses RefCell to defer checking to run-time (hiding mutability), So borrow checker accepts it. I've added a new case using predicate p3 that doesn't satisfy ownership rules even at run-time, and so panics. */ fn test_checker3(p1: bool, p2: bool, p3:bool) { print!( "\n -- test_checcker3({}, {}, {}) --", p1, p2, p3 ); let s = String::from("this is a test"); // replacement let sp1 = RefCell::new(s); print!("\n &sp1 = {:?}", &sp1.borrow()); let sp2 = RefCell::new("rsp2".to_string()); // initial val let mut rsp2 = &sp2; let sp3 = RefCell::new("rsp3".to_string()); // initial val let mut rsp3: &RefCell<String> = &sp3; if p1 { print!("\n -- immutable borrow --"); rsp2 = &sp1; print!("\n &rsp2.borrow() = {:?}", &rsp2.borrow()); } // borrow goes out of scope if p2 { print!("\n -- mutable borrow --"); let mut x = sp1.borrow_mut(); x.push_str(" and more"); rsp3 = &sp1; print!("\n &rsp3 = {:?}", &rsp2); } // borrow goes out of scope if p3 { // panic if p3 true print!("\n -- immutable and mutable borrow --"); let _ = std::io::stdout().flush(); rsp2 = &sp1; // immutable borrow let mut x = sp1.borrow_mut(); // mutable borrow x.push_str(" and more"); // mutates sp1 inner /*-- looks like immutable borrow so compiles --*/ rsp3 = &sp1; print!("\n &rsp2.borrow() = {:?}", &rsp2.borrow()); print!("\n &rsp3.borrow() = {:?}", &rsp3.borrow()); } // never get here - ownership rules violated print!("\n -- final results --"); print!("\n &rsp2.borrow() = {:?}", &rsp2.borrow()); print!("\n &rsp3.borrow() = {:?}", &rsp3.borrow()); putline(); } fn main() { // test_checker1(true); // succeeds // test_checker1(false); // succeeds test_checker3(true, true, false); // succeeds test_checker3(true, true, true); // panics println!("\n\n That's all Folks!\n\n"); } Output: -- test_checcker3(true, true, false) -- &sp1 = "this is a test" -- immutable borrow -- &rsp2.borrow() = "this is a test" -- mutable borrow -- &rsp3 = RefCell { value: } -- final results -- &rsp2.borrow() = "this is a test and more" &rsp3.borrow() = "this is a test and more" -- test_checcker3(true, true, true) -- &sp1 = "this is a test" -- immutable borrow -- &rsp2.borrow() = "this is a test" -- mutable borrow -- &rsp3 = RefCell { value: } -- immutable and mutable borrow -- thread 'main' panicked at 'already mutably borrowed: BorrowError', ... Deferring checking to run-time doesn't let you violate the ownership rules. You use this technique when the compiler's static analysis can't tell if the rules will be broken. In this case, unless you do something like this, the build will fail. Using RefCell hides the mutability of a borrow, which allows the code to compile, and then run-time processing will succeed or panic depending on whether ownership rules are actually satisfied or not. This turns out to be a very important technique for sharing between threads, as we will see in the Threads Bite.
9. References:
Reference | Description |
---|---|
Code for SmartPtrs | Code for Examples of Box, Rc, Arc, RefCell |
Jon Gjengset | Crust of Rust: SmartPtrs and interior mutability |