Cell and RefCell — Wrapper Types in Rust In Depth with Examples

In the last post, we saw that Box Pointer as a unique smart pointer that can be used to initialize values on the heap or of unknown size. In this post, we’ll take a look at Cell and RefCellwrapper types , how to use them and more importantly how not to use them. Both Cell and RefCell in rust act as a Container type that allow interior mutability. These are types that kind of skip borrow checking, and can prove dangerous when not used properly at runtime. Let’s take a look at what this really means by looking at interior mutability.

Interior mutability

Rust takes mutability very seriously. Ownership rules prevent developers from doing something that may have side effects. For most basic operations, we can only mutate a value when we explicitly declare a variable as mutable. And then the compiler performs checks to ensure we don’t violate any ownership and mutability rules.

Often times you may want to mutate values without explicitly taking a mutable reference. These are the cases when Cell and RefCell types come in handy.

A Cell<T> or RefCell<T> type in rust is a container that allows developers to have shared mutability of the any underlying variable. Let’s take a look at the sample below.

use std::cell::Cell;  
  
fn main() {  
    let num = 23 ;  
    let i = Cell::new(num);  
    println!("original value  is {}", i.get());  
    add_num_by_10(&i);  
    println!("Value after adding is {}." , i.get())  
}  
  
fn add_num_by_10( val : &Cell<i32>){  
    val.set(val.get()+10)  
}
original value  is 23  
Value after adding is 33.

Upon execution of the above program, we see that the value of the cell reference variable i got updated to 33 in the end. Notice how we passed an immutable reference, and yet we updated the value of the value inside our container. There are two things to note here.

  1. When we initialized our Cell Type, we copied the variable num into i. The original variable num still holds 10 by the end of the program execution.
  2. When a get operation is performed on a Cell container, we get a copy of the value.
  3. The value inside the Cell container can be updated using the set method.

Another example to demonstrate this is show below.

use std::cell::Cell;  
  
fn main() {  
    let num = 23 ;  
    println!("Address of original value num is {:p}", &num);  
    let i = Cell::new(num);  
    println!("Address of value in i is {:p}", &i.get());  
    get_num_value(&i)  
}

Address of original value num is 0x7fff03304e54  
Address of value in i is 0x7fff03304ef4  
Address of variable inside the function i is 0x7fff03304e3c

Notice how the addresses of all variables are different. In the end the value that the Cell holds is what matters. Cell works relatively fast on primitive types and copy traits by copying values. Such copying can be expensive with non primitives / large struct types. We may want to actually store an object that may be on a heap and work with these objects.

Now let’s see an example with a non primitive type that doesn’t implement the Copy trait and see what happens.

use std::cell::Cell;  
  
struct Feature {  
    name : String  
}  
  
impl Feature {  
    fn new(name : String) -> Self{  
        return Feature{name}  
    }  
}  
  
fn main() {  
    let feature = Feature::new("Dashboard v2".to_owned());   
    let i = Cell::new(feature);  
    println!("original value  is {}", i.get().name);  
}

When this program is executed, we get the following error.

error[E0599]: the method `get` exists for struct `Cell<Feature>`, but its trait bounds were not satisfied  
  --> src/main.rs:16:41  
   |  
3  | struct Feature {  
   | --------------  
   | |  
   | doesn't satisfy `Feature: Clone`  
   | doesn't satisfy `Feature: Copy`  
...  
16 |     println!("original value  is {}", i.get().name);  
   |                                         ^^^  
   |  
   = note: the following trait bounds were not satisfied:  
           `Feature: Copy`  
           `Feature: Clone`  
           which is required by `Feature: Copy`  
help: consider annotating `Feature` with `#[derive(Clone, Copy)]`  
   |  
3  | #[derive(Clone, Copy)]  
   |

To mitigate this error , we have the RefCell type.

RefCell is very similar to Cell except that it doesn’t copy values. It holds a reference to a type. Refer to the program below with the fix.

use std::cell::{Cell, RefCell};  
  
struct Feature {  
    name : String  
}  
  
impl Feature {  
    fn new(name : String) -> Self{  
        return Feature{name}  
    }  
}  
  
fn main() {  
    let feature = Feature::new("Dashboard v2".to_owned());  
    let i = RefCell::new(feature);  
    println!("original value  is {}", i.borrow().name);  
  
}

original value is Dashboard v2

Since a RefCel holds a reference, we have to borrow a value to the underlying reference. The three methods that may typically be used are borrow ,borrow_mut, and into_inner to borrow immutable and mutable references . Note that there can be many immutable references to a RefCell.

struct Feature {  
  name : String,  
  version : f32  
}  

impl Feature {  
  fn new(name : String) -> Self{  
      return Feature{name, version:0.0}  
  }  
}  

fn main() {  
  let feature = Feature::new("Dashboard v2".to_owned());  
  let i = RefCell::new(feature);  
  update_feature_name(&i);  
  println!("Name : {} , version :{} " , i.borrow().name, i.borrow().version) ;  
}  

fn update_feature_name(feature_ref : &RefCell<Feature>){  
  feature_ref.borrow_mut().name = "New feature Name ".to_owned();  
  update_version(&feature_ref);  
}

This means we can pass around Cell types immutably and change values within methods, even within methods. There is however, a catch with this.

Cell and RefCell are not thread safe.

It is important to understand that both Cell and RefCell types in Rust are not thread safe. They are intended to be used only on a single thread. For the purposes of this article, all operations happen only on one thread.

Errors Can be Caught when using RefCell

RefCell allows us to try a borrow instead of just requesting a borrow. the method try_borrowborrows the wrapped value, returning an error if the value is currently mutably borrowed. Any errors can be matched and appropriate actions can be taken.

Interior mutability comes with a Risk

In the simple examples above, we saw how interior mutability can be achieved by using a RefCell / cell Wrapper. This is acheived by avoiding some compiler checks on borrow rules which would o otherwise fail our program.

However, a runtime panic may occur if a violation to the borrowing rules occur at runtime. This can be dangerous. I always like to look at examples where things may break. Below is one for using the RefCel type.

use std::cell::{RefCell};  
  
struct Feature {  
    name : String,  
    version : f32  
}  
  
impl Feature {  
    fn new(name : String) -> Self{  
        return Feature{name, version:0.0}  
    }  
}  
  
fn main() {  
    let feature = Feature::new("Dashboard v2".to_owned());  
    let book_ref = RefCell::new(feature);  
    {  
        let mut_book = book_ref.borrow_mut();  
        {  
            // we already borrowed a mutable reference in the parent block.  
            // this will cause the code to panic because the check  
            // happens at runtime.  
            let immutable_book = book_ref.borrow();  
        }  
    }  
}

We first borrowed the book variable mutably. And immediately borrowed the book immutably. Imagine this happening in two totally different methods in different modules. This program compiles just file, but panics at runtime with the error below.

thread 'main' panicked at 'already mutably borrowed: BorrowError',   
   5: rust_wrapper_types::main  
             at ./src/main.rs:23:34  
stack backtrace:  
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

This is not a situation we may want to be while writing programs. Imagine the exact same program written without the use of RefCell. The compiler would actually check if we are working with mutable references and prevent mistakes like this.

Conclusion

In summary , one can use RefCell in Rust only when you explicitly need to skip the compiler checks for borrowing and really need the functionality. Prefer using & referencesinstead wherever possible — because improper usage of these wrapper types may cause programs to crash.

We’ve seen Box Pointer usage , Cell, and RefCell until now. Each of these types is a container to hold data and perform operations on these while providing some flexibility. In the next few articles, we’ll see on how to have multiple owners to the same data in Rust. How can we share these objects across different variables immutably and mutably, and across multiple threads safely — using Rc and Arc types.