about
03/15/2023
RustBites - SmartPtrs
Rust Bites Code

Rust Bite - SmartPtrs

Box, Rc, Arc, RefCell, interior mutability,

The Rust std library provides an interesting set of smart pointers. The Box<T> pointer provides the only way to store and access data on the heap. Rc<T> and Arc<T> are reference counted pointers designed to support sharing of data for single threads and multiple threads, respectively. RefCell<T> supports deferring borrow checking for ownership from compile-time to run-time. That is essential in some multi-threaded applications. This Bite is divided into sections for each of the pointers. There we discuss each pointer's purpose and ways to create them and use their contents.

1. std::boxed::Box<T>

std::Box<T> is a smart pointer to data in the heap. Box manages allocation and deallocation of heap memory, and provides an interface to interact with that data.
Box - smart pointer to allocation on heap let sp = Box::new(T::new()); /*--------------------------------------------*/ Box automatically derefernces to a T so you can call T methods on sp /*--------------------------------------------*/ let vl = sp.getter(); // T method sp.setter(vl); // T method let spc = sp.clone(); // Box method let t = *sp; // move heap value to stack, drops Box // Box deallocates heap memory in its drop method
An example of Box code is shown in the dropdown, below.
Example: Box<T> show_box::main.rs ///////////////////////////////////////////////////////////// // show_box::main.rs - demo Box<T> pointer to heap // // // // Jim Fawcett, https://JimFawcett.github.io, 06 Jun 2020 // ///////////////////////////////////////////////////////////// #![allow(clippy::print_literal)] use core::fmt::{ Debug }; /*-- same as print!("\n sp = {:?}", sp) --*/ fn show<T:Debug>(name:&str, t:&T) { print!("\n {} = {:?}", name, t); } fn main() { let putline = || print!("\n"); type TLC = test_type::TestLifeCycle; print!("\n -- create Box pointer to TLC on heap --"); let sp = Box::new(TLC::new()); show("sp", &sp); putline(); print!("\n -- access member of TLC using Box --"); let v = sp.get_value(); show("v ", &v); putline(); print!("\n -- clone Box pointer to TLC on heap --"); let mut spc = sp.clone(); show("spc", &spc); putline(); print!("\n -- mutate TLC through Box --"); spc.set_value(42); show("spc", &spc); show("v ", &spc.get_value()); show("sp", &sp); putline(); print!("\n -- create reference to Box pointer --"); let rspc = &mut spc; rspc.set_value(21); show("rspc", &rspc); // holds new value show("spc", &spc); // holds new value too putline(); print!( "\n -- move contents of Box to stack with deref --" ); let tlc = *sp; // move show("tlc", &tlc); // statement below won't compile since sp has been moved // show("sp", &sp); println!("\n\n That's all Folks!"); } /*-- drop Boxed item --*/ Output: cargo run -q -- create Box pointer to TLC on heap -- creating instance 0 of TestLifeCycle sp = TestLifeCycle { count: 0 } -- access member of TLC using Box -- v = 0 -- clone Box pointer to TLC on heap -- TestLifeCycle instance 0 cloned spc = TestLifeCycle { count: 0 } -- mutate TLC through Box -- spc = TestLifeCycle { count: 42 } v = 42 sp = TestLifeCycle { count: 0 } -- create reference to Box pointer -- rspc = TestLifeCycle { count: 21 } spc = TestLifeCycle { count: 21 } -- move contents of Box to stack with deref -- tlc = TestLifeCycle { count: 0 } That's all Folks! TestLifeCycle instance 0 dropped TestLifeCycle instance 21 dropped

2. std::rc::Rc<T>

std::rc::Rc<T> is a reference counted pointer to an initialized value on the stack. Rc manages multiple references by dropping its inner value only when the last Rc pointer is dropped, e.g., there are no dangling references.
Rc - reference counted pointer let tlc = test_type::TestLifeCycle; let mut rc1 = Rc::new(tlc) /*-------------------------------------------------- std::rc::Rc is a reference counted pointer. - Rc::new(value) creates a pointer to value. - Rc::clone creates a new pointer to the same source inner value, adding 1 to ref count. - Rc automatically dereferences to a T, so you can call T methods on an Rc<T>. - Rc::drop decrements the ref count, dropping the inner value on zero. - Note, it does no harm to drop a stack value as long as there is no attempt to use after. --------------------------------------------------*/ let rc2 = rc1.clone(); let rc3 = Rc::clone(&rc2); let opt = Rc::get_mut(&rc1); // where opt: Option<Some(&mut tlc)> /*-------------------------------------------------- get_mut returns an Option because the Rust ownership rules allow only one active mutable reference. If there is already an active mutable reference the option contains the enumeration value None. Otherwise it contains Some(&mut inner_value). --------------------------------------------------*/
Example code for Rc is shown below. Note that most of the code complexity results from processing an Option, returned by Rc::get_mut(&mut inner_value). That's the way the cookie crumbles.
Example: Rc<T> show_rc::main.rs ///////////////////////////////////////////////////////////// // show_rc::main.rs - demo Rc<T> ref counted ptr // // // // Jim Fawcett, https://JimFawcett.github.io, 06 Jun 2020 // ///////////////////////////////////////////////////////////// use core::fmt::{ Debug }; use std::rc::Rc; /*-- same as print!("\n sp = {:?}", sp) --*/ #[allow(clippy::print_literal)] fn show<T:Debug>(name:&str, t:&T) { print!("\n {} = {:?}", name, t); } struct SpaceAtEnd; impl Drop for SpaceAtEnd { fn drop(&mut self) { print!("\n\n"); } } fn main() { let putline = || print!("\n"); let _cosmetic = SpaceAtEnd; print!( "\n -- create instance of TestLifeCycle in stack --" ); let tlc = test_type::TestLifeCycle::new(); putline(); print!("\n -- create Rc ptr to value in stack --"); let mut rc1 = Rc::new(tlc); show("rc1", &rc1); show( "Rc strong count", &Rc::strong_count(&rc1) ); show("rc1 inner value", &rc1.get_value()); putline(); print!( "\n -- attempt to mutate inst through Rc ptr --" ); let opt_mut_rc1 = Rc::get_mut(&mut rc1); match opt_mut_rc1 { Some(val) => { /*-- make mutable reference --*/ let mut_rc1 = &mut *val; mut_rc1.set_value(42); show("rc1", &rc1); }, None => { print!( "\n can't have multiple mutable references" ); } } putline(); print!( "\n -- create new Rc ptr to inst in stack --" ); let mut rc2 = rc1.clone(); show("rc2", &rc2); show( "Rc strong count", &Rc::strong_count(&rc2) ); putline(); print!( "\n -- attempt to mutate inst through Rc ptr --" ); let opt_mut_rc2 = Rc::get_mut(&mut rc2); match opt_mut_rc2 { Some(val) => { /*-- make mutable reference --*/ let mut_rc2 = &mut *val; mut_rc2.set_value(21); show("rc2", &rc2); }, None => { print!( "\n can't have multiple mutable references" ); } } putline(); /*-- alternate clone syntax --*/ print!( "\n -- create another new Rc ptr to inst in stack --" ); let mut rc3 = Rc::clone(&rc1); show("rc3", &rc3); show("Rc strong count", &Rc::strong_count(&rc1)); putline(); /*-- alternate matching syntax --*/ print!("\n -- attempt to mutate inst through Rc ptr --"); let opt_mut_rc3 = Rc::get_mut(&mut rc3); if let Some(val) = opt_mut_rc3 { /*-- make mutable reference --*/ let mut_rc3 = &mut *val; mut_rc3.set_value(21); show("rc3", &rc2); } else { print!( "\n can't have multiple mutable references" ); } putline(); // print!("\n dropping rc1"); // drop(rc1); // print!("\n dropping rc2"); // drop(rc2); // print!("\n dropping rc3"); // drop(rc3); // putline(); /*------------------------------------------------------- Help with Option processing, used above: ----------------------------------------------------- The code below illustrates destructuring an option with both if let and match constructs. These will be discussed again in the Error Handling Bite. */ let maybe_cake = Some("this code is a piece of cake! :-)"); let not_cake = std::option::Option::<&str>::None; if let Some(cake) = maybe_cake { // cake is defined due to print!("\n {}", cake); // if let destructuring } let maybe_cake: Option<&str> = not_cake; match maybe_cake { Some(cake) => print!("\n {}", cake), // now, cake is defined by // match destructuring None => print!( "\n this code is not a piece of cake!! :-(" ) } println!("\n\n That's all Folks!"); } Output: cargo run -q -- create instance of TestLifeCycle in stack -- creating instance 0 of TestLifeCycle -- create Rc ptr to value in stack -- rc1 = TestLifeCycle { count: 0 } Rc strong count = 1 rc1 inner value = 0 -- attempt to mutate inst through Rc ptr -- rc1 = TestLifeCycle { count: 42 } -- create new Rc ptr to inst in stack -- rc2 = TestLifeCycle { count: 42 } Rc strong count = 2 -- attempt to mutate inst through Rc ptr -- can't have multiple mutable references -- create another new Rc ptr to inst in stack -- rc3 = TestLifeCycle { count: 42 } Rc strong count = 3 -- attempt to mutate inst through Rc ptr -- can't have multiple mutable references this code is a piece of cake! :-) this code is not a piece of cake!! :-( That's all Folks! TestLifeCycle instance 42 dropped
Rc is used most often with RefCell to enable Interior Mutability. You will see discussion and examples later in this Bite.

3. std::sync::Arc<T>

std::sync::Arc<T> is a thread-safe reference counted pointer to an initialized value on the stack. Arc manages multiple references by dropping its inner value only when the last Rc pointer is dropped, e.g., there are no dangling references. Arc is a thread-safe version of Rc.
Arc - atomic reference counted pointer let tlc = test_type::TestLifeCycle; let mut arc1 = Arc::new(tlc) /*-------------------------------------------------- std::sync::Arc is a thread-safe reference counted pointer, very like Rc, but using atomic ref count. - Arc::new(value) creates a pointer to value. - Arc::clone creates a new pointer to the same source inner value, adding 1 to ref count. - Arc::drop decrements the ref count, dropping the inner value on zero. - Note, it does no harm to drop a stack value as long as there is no attempt to use after. --------------------------------------------------*/ let arc2 = arc1.clone(); let arc3 = Arc::clone(&arc2); let opt = Arc::get_mut(&arc1); where opt: Option<Some(&mut tlc)>
Example code for Arc is shown below. Note that most of the code complexity results from processing an Option, returned by Arc::get_mut(&mut inner_value). That's still the way cookies crumble.
Example: Arc<T> show_arc::main.rs ///////////////////////////////////////////////////////////// // show_arc::main.rs - demo Arc<T> ref counted ptr // // // // Jim Fawcett, https://JimFawcett.github.io, 06 Jun 2020 // ///////////////////////////////////////////////////////////// use core::fmt::{ Debug }; use std::sync::Arc; /*-- same as print!("\n sp = {:?}", sp) --*/ #[allow(clippy::print_literal)] fn show<T:Debug>(name:&str, t:&T) { print!("\n {} = {:?}", name, t); } struct SpaceAtEnd; impl Drop for SpaceAtEnd { fn drop(&mut self) { print!("\n\n"); } } fn main() { let putline = || print!("\n"); let _cosmetic = SpaceAtEnd; print!( "\n -- create instance of TestLifeCycle in stack --" ); let tlc = test_type::TestLifeCycle::new(); putline(); print!("\n -- create Arc ptr to value in stack --"); let mut arc1 = Arc::new(tlc); show("arc1", &arc1); show( "Arc strong count", &Arc::strong_count(&arc1) ); show("arc1 inner value", &arc1.get_value()); putline(); print!( "\n -- attempt to mutate inst through Arc ptr --" ); let opt_mut_arc1 = Arc::get_mut(&mut arc1); match opt_mut_arc1 { Some(val) => { /*-- make mutable reference --*/ let mut_arc1 = &mut *val; mut_arc1.set_value(42); show("arc1", &arc1); }, None => { print!( "\n can't have multiple mutable references" ); } } putline(); print!("\n -- create new Arc ptr to inst in stack --"); let mut arc2 = arc1.clone(); show("arc2", &arc2); show( "Arc strong count", &Arc::strong_count(&arc2) ); putline(); print!( "\n -- attempt to mutate inst through Arc ptr --" ); let opt_mut_arc2 = Arc::get_mut(&mut arc2); match opt_mut_arc2 { Some(val) => { /*-- make mutable reference --*/ let mut_arc2 = &mut *val; mut_arc2.set_value(21); show("arc2", &arc2); }, None => { print!( "\n can't have multiple mutable references" ); } } putline(); /*-- alternate clone syntax --*/ print!( "\n -- create another new Arc ptr to inst in stack --" ); let mut arc3 = Arc::clone(&arc1); show("arc3", &arc3); show( "Arc strong count", &Arc::strong_count(&arc1) ); putline(); /*-- alternate matching syntax --*/ print!( "\n -- attempt to mutate inst through Arc ptr --" ); let opt_mut_arc3 = Arc::get_mut(&mut arc3); if let Some(val) = opt_mut_arc3 { /*-- make mutable reference --*/ let mut_arc3 = &mut *val; mut_arc3.set_value(21); show("arc3", &arc2); } else { print!( "\n can't have multiple mutable references" ); } putline(); // print!("\n dropping rc1"); // drop(rc1); // print!("\n dropping rc2"); // drop(rc2); // print!("\n dropping rc3"); // drop(rc3); // putline(); println!("\n\n That's all Folks!"); } Output: cargo run -q -- create instance of TestLifeCycle in stack -- creating instance 0 of TestLifeCycle -- create Arc ptr to value in stack -- arc1 = TestLifeCycle { count: 0 } Arc strong count = 1 arc1 inner value = 0 -- attempt to mutate inst through Arc ptr -- arc1 = TestLifeCycle { count: 42 } -- create new Arc ptr to inst in stack -- arc2 = TestLifeCycle { count: 42 } Arc strong count = 2 -- attempt to mutate inst through Arc ptr -- can't have multiple mutable references -- create another new Arc ptr to inst in stack -- arc3 = TestLifeCycle { count: 42 } Arc strong count = 3 -- attempt to mutate inst through Arc ptr -- can't have multiple mutable references That's all Folks! TestLifeCycle instance 42 dropped
Arc is used most often with Threading applications. You will see its use in examples for that Bite.

4. std::cell::RefCell<T>

RefCell<T> is used to defer borrow checking until run-time. We declare it as an immutable type, but are allowed to borrow a mutable reference. If we already have an active borrow, the program will panic, because the ownership rules have been violated. The point of all this is that there are certain conditions under which correct code will not compile because the compiler's static analysis can't guarantee that ownership rules haven't been violated. RefCell<T> appears to the compiler to be immutable, but supports return of mutable references to its inner value. This allows code to compile, and wait until run-time to verify that the ownership rules have or have not been satisfied. A common place where this occurs is in mult-threaded code. We will see some examples when we look at the Threads Bite. When we borrow from a RefCell<T> we get a Ref<T> or a RefMut<T>, depending on the type of borrow. These types are essentially transparent because they support automatic dereferencing to the RefCell<T> inner value.
RefCell, Ref, and RefMut RefCell holds a mutable memory location with dynamically checked borrow rules. let sp = std::cell::RefCell::new(T::new()); let r1 = sp.borrow() // returns a Ref<T> which impl's Deref // panics if r1 was mutably borrowed // access inner with sp.borrow() // due to auto dereferencing let r2 = sp.borrow_mut() // returns a RefMut<T> which impl's Deref // panics if r2 was already borrowed // access inner with sp.borrow_mut() // due to auto dereferencing let t = sp.into_inner() // moves inner to t let t0 = sp.replace(t1) // replaces original inner with new std::mem::drop(r1) // drops inner t
The example, below, illustrates the basic mechanics for using RefCell<T> and the wrappers Ref<T> and RefMut<T>. It doens't give you much insight into how they are used in practice. That will have to wait until we get to Threads. There we will see that Mutex<T> and RwLock<T> need RefCell to implement their functionality.
Example: RefCell<T> show_refcell::main.rs ///////////////////////////////////////////////////////////// // show_refcell::main.rs - demo RefCell smart pointer // // // // Jim Fawcett, https://JimFawcett.github.io, 06 Jun 2020 // ///////////////////////////////////////////////////////////// #![allow(clippy::print_literal)] #![allow(unused_imports)] use core::fmt::{ Debug }; use std::cell::{ RefCell, Ref, RefMut }; use std::any::type_name; /*-- same as print!("\n sp = {:?}", sp) --*/ fn show(name:&str, t:&T) { print!("\n {} = {:?}", name, t); } fn show_type<'a, T:Debug>(_t:T) -> &'a str where T:Debug { type_name:: () } struct SpaceAtEnd; impl Drop for SpaceAtEnd { fn drop(&mut self) { print!("\n\n"); } } fn main() { /*------------------------------------------------------- Note: This program will panic if both Blk #1 and Blk #2 are uncommented. That's because the borrowing rules are violated. If one is commented out - it doesn't matter which one - the program completes successfully. This illustrates that RefCell defers borrow checking to run-time. */ let putline = || print!("\n"); let _cosmetic = SpaceAtEnd; let tlc = test_type::TestLifeCycle::new(); print!("\n -- create RefCell pointer to tlc --"); /*-- note sp is statically immutable --*/ let sp = RefCell::new(tlc); show("sp", &sp); putline(); // Blk #1 // print!("\n -- borrow inner TLC from RefCell --"); // let b = sp.borrow(); // show("b ", &b); // let v = b.get_value(); // show("v ", &v); // putline(); // Blk #2 print!( "\n -- mutably borrow inner TLC from RefCell --" ); /*------------------------------------------------------- - Even though sp is immutable, we can get a mutable reference, b2. - RefCell defers borrow checking to runtime. - Succeeds only if there are no other current borrows. You can see that by uncommenting the code in blk #1 */ let mut b2 = sp.borrow_mut(); // returns RefMut show("b2", &b2); b2.set_value(42); // automatically dereferenced show("b2", &b2); let v = b2.get_value(); // here too show("v ", &v); putline(); print!("\n -- exploring borrowed type --"); type TLC = test_type::TestLifeCycle; let sp2 = RefCell::new(TLC::new()); let r:Ref = sp2.borrow(); show("r:Ref", &show_type(r)); putline(); print!( "\n-- exploring automatic dereferencing of Ref --" ); /*-- r was moved in show_type(r) so get another --*/ let r = sp2.borrow(); let tlc = &*r; show("r:Ref", &r); show("tlc", &tlc); putline(); print!("\n -- exploring mutably borrowed type --"); let sp2 = RefCell::new(TLC::new()); let r:RefMut = sp2.borrow_mut(); show("r:RefMut", &show_type(r)); putline(); print!( "\n-- exploring automatic dereferencing of RefMut--" ); /*-- r was moved in show_type(r) so get another --*/ let r = sp2.borrow_mut(); let tlc = &*r; show("r:RefMut", &r); show("tlc", &tlc); println!("\n\n That's all Folks!"); } Output: creating instance 0 of TestLifeCycle -- create RefCell pointer to tlc -- sp = RefCell { value: TestLifeCycle { count: 0 } } -- mutably borrow inner TLC from RefCell -- b2 = TestLifeCycle { count: 0 } b2 = TestLifeCycle { count: 42 } v = 42 -- exploring borrowed type -- creating instance 0 of TestLifeCycle r:Ref = "core::cell::Ref" -- exploring automatic dereferencing of Ref -- r:Ref = TestLifeCycle { count: 0 } tlc = TestLifeCycle { count: 0 } -- exploring mutably borrowed type -- creating instance 0 of TestLifeCycle r:RefMut = "core::cell::RefMut" -- exploring automatic dereferencing of RefMut -- r:RefMut = TestLifeCycle { count: 0 } tlc = TestLifeCycle { count: 0 } That's all Folks! TestLifeCycle instance 0 dropped TestLifeCycle instance 0 dropped TestLifeCycle instance 42 dropped
If you look through the Rust documentation you will find other applications of RefCell<T> for building Mock Objects for testing and for initializing objects that have an immutable interface.

5. Interior Mutability

RefCell<T> was designed to support something called "interior mutability". Its inner element can be mutated throught its member function borrow_mut(). That returns a reference that appears to the compiler to be immutable, but which supports mutating the inner element. That doesn't escape the Rust ownership rules. It just defers their application to run-time. When borrow_mut() is invoked the borrow is recorded and, should another borrow occur before the first becomes inactive, then a panic results. The advantage of this is that the compiler's borrow checker cannot always tell if a time sequence of borrows is safe, and so will fail to compile unless we use RefCell<T>.

6. Conclusions:

For single-threaded applications, the most frequently used smart pointer is the Box<T> to access data stored heap. All except Rc<T> are used extensively in multi-threaded applications.

7. Exercises:

  1. Store a string in the heap, and pass around access to several functions. Why could that be useful?
  2. The smart pointers Rc and RefCell are often used together as Rc<RefCell<T>>. What is the purpose of this construct?
  3. 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

In this section we look at several exercises that are a bit more complicated. I've provided solutions for them because of that, but its worth your time to read them as they will help you understand the intended roles for each of these smart pointers.
  1. 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.
  2. 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.
  3. 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
  Next Prev Pages Sections About Keys