How do I work with Cell for Interior Mutability in Rust?

Walkthrough

Cell provides interior mutability for Copy types. It allows you to mutate the inner value through a shared reference (&T), bypassing Rust's normal borrowing rules. This is achieved by moving values in and out rather than through direct mutation.

Key concepts:

  • Only for Copy types — Cell<T> requires T: Copy
  • No direct references — Cannot get &T or &mut T to the inner value
  • Move semantics — Values are moved in and out
  • Single-threaded — Cell is not thread-safe (!Sync)

When to use Cell:

  • Mutating small Copy types through shared references
  • Implementing immutable-looking APIs with internal state
  • Simple counters and flags
  • Caching computed values

When NOT to use Cell:

  • Non-Copy types (use RefCell instead)
  • Thread-safe mutation (use AtomicXxx or Mutex)
  • When you need references to the inner value

Code Examples

Basic Cell Usage

use std::cell::Cell;
 
fn main() {
    let cell = Cell::new(42);
    
    // Get a copy of the value
    let value = cell.get();
    println!("Initial value: {}", value);
    
    // Set a new value (through shared reference!)
    cell.set(100);
    println!("New value: {}", cell.get());
}

Cell with Shared References

use std::cell::Cell;
 
struct Counter {
    count: Cell<u32>,
}
 
impl Counter {
    fn new() -> Self {
        Self {
            count: Cell::new(0),
        }
    }
    
    fn increment(&self) {
        // Mutate through &self!
        let current = self.count.get();
        self.count.set(current + 1);
    }
    
    fn get(&self) -> u32 {
        self.count.get()
    }
}
 
fn main() {
    let counter = Counter::new();
    
    // Multiple shared references can mutate
    let ref1 = &counter;
    let ref2 = &counter;
    
    ref1.increment();
    ref2.increment();
    counter.increment();
    
    println!("Count: {}", counter.get());
}

Cell for Caching

use std::cell::Cell;
 
struct CachedValue<T> {
    value: Cell<Option<T>>,
    computed: Cell<bool>,
}
 
impl<T: Copy> CachedValue<T> {
    fn new() -> Self {
        Self {
            value: Cell::new(None),
            computed: Cell::new(false),
        }
    }
    
    fn get_or_compute<F>(&self, compute: F) -> T
    where
        F: FnOnce() -> T,
    {
        if !self.computed.get() {
            self.value.set(Some(compute()));
            self.computed.set(true);
        }
        self.value.get().unwrap()
    }
    
    fn invalidate(&self) {
        self.computed.set(false);
        self.value.set(None);
    }
}
 
fn main() {
    let cache = CachedValue::new();
    
    let value = cache.get_or_compute(|| {
        println!("Computing...");
        42
    });
    println!("Value: {}", value);
    
    let cached = cache.get_or_compute(|| {
        println!("Computing again...");
        100
    });
    println!("Cached: {}", cached);
}

Cell with Enums

use std::cell::Cell;
 
#[derive(Clone, Copy)]
enum State {
    Idle,
    Running,
    Paused,
    Stopped,
}
 
struct Machine {
    state: Cell<State>,
}
 
impl Machine {
    fn new() -> Self {
        Self {
            state: Cell::new(State::Idle),
        }
    }
    
    fn start(&self) {
        match self.state.get() {
            State::Idle | State::Paused => {
                self.state.set(State::Running);
                println!("Started");
            }
            _ => println!("Cannot start from current state"),
        }
    }
    
    fn pause(&self) {
        if self.state.get() == State::Running {
            self.state.set(State::Paused);
            println!("Paused");
        }
    }
    
    fn stop(&self) {
        self.state.set(State::Stopped);
        println!("Stopped");
    }
}
 
fn main() {
    let machine = Machine::new();
    machine.start();
    machine.pause();
    machine.start();
    machine.stop();
}

Cell with Structs

use std::cell::Cell;
 
#[derive(Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}
 
struct Transform {
    position: Cell<Point>,
}
 
impl Transform {
    fn new(x: i32, y: i32) -> Self {
        Self {
            position: Cell::new(Point { x, y }),
        }
    }
    
    fn move_by(&self, dx: i32, dy: i32) {
        let mut pos = self.position.get();
        pos.x += dx;
        pos.y += dy;
        self.position.set(pos);
    }
    
    fn position(&self) -> Point {
        self.position.get()
    }
}
 
fn main() {
    let transform = Transform::new(0, 0);
    println!("Position: ({}, {})", transform.position().x, transform.position().y);
    
    transform.move_by(10, 20);
    println!("After move: ({}, {})", transform.position().x, transform.position().y);
}

Cell for Toggle Flags

use std::cell::Cell;
 
struct Toggle {
    state: Cell<bool>,
}
 
impl Toggle {
    fn new(initial: bool) -> Self {
        Self {
            state: Cell::new(initial),
        }
    }
    
    fn toggle(&self) {
        self.state.set(!self.state.get());
    }
    
    fn is_on(&self) -> bool {
        self.state.get()
    }
}
 
fn main() {
    let toggle = Toggle::new(false);
    println!("Initial: {}", toggle.is_on());
    
    toggle.toggle();
    println!("After toggle: {}", toggle.is_on());
    
    toggle.toggle();
    println!("After another toggle: {}", toggle.is_on());
}

Cell::replace and Cell::swap

use std::cell::Cell;
 
fn main() {
    let cell = Cell::new(42);
    
    // Replace the value and get the old one
    let old = cell.replace(100);
    println!("Old: {}, New: {}", old, cell.get());
    
    // Swap two cells
    let a = Cell::new(1);
    let b = Cell::new(2);
    
    a.swap(&b);
    println!("a: {}, b: {}", a.get(), b.get());
}

Cell::take

use std::cell::Cell;
 
fn main() {
    let cell = Cell::new(Some(42));
    
    // Take the value, leaving Default::default()
    let value = cell.take();
    println!("Took: {:?}", value);
    println!("After take: {:?}", cell.get());
}

Cell in Collections

use std::cell::Cell;
use std::collections::HashMap;
 
struct VisitedTracker {
    visited: HashMap<String, Cell<bool>>,
}
 
impl VisitedTracker {
    fn new() -> Self {
        Self {
            visited: HashMap::new(),
        }
    }
    
    fn mark_visited(&self, key: &str) {
        self.visited
            .entry(key.to_string())
            .or_insert(Cell::new(false))
            .set(true);
    }
    
    fn is_visited(&self, key: &str) -> bool {
        self.visited.get(key).map_or(false, |c| c.get())
    }
}
 
fn main() {
    let tracker = VisitedTracker::new();
    
    tracker.mark_visited("page1");
    tracker.mark_visited("page2");
    
    println!("page1 visited: {}", tracker.is_visited("page1"));
    println!("page3 visited: {}", tracker.is_visited("page3"));
}

Cell for Reference Counting Simulation

use std::cell::Cell;
use std::ptr::NonNull;
 
struct SimpleRcInner<T> {
    value: T,
    ref_count: Cell<usize>,
}
 
pub struct SimpleRc<T> {
    inner: NonNull<SimpleRcInner<T>>,
}
 
impl<T> SimpleRc<T> {
    pub fn new(value: T) -> Self {
        let inner = Box::new(SimpleRcInner {
            value,
            ref_count: Cell::new(1),
        });
        Self {
            inner: unsafe { NonNull::new_unchecked(Box::into_raw(inner)) },
        }
    }
    
    pub fn get(&self) -> &T {
        unsafe { &self.inner.as_ref().value }
    }
    
    fn ref_count(&self) -> usize {
        unsafe { self.inner.as_ref().ref_count.get() }
    }
}
 
impl<T> Clone for SimpleRc<T> {
    fn clone(&self) -> Self {
        unsafe {
            self.inner.as_ref().ref_count.set(
                self.inner.as_ref().ref_count.get() + 1
            );
        }
        Self { inner: self.inner }
    }
}
 
impl<T> Drop for SimpleRc<T> {
    fn drop(&mut self) {
        unsafe {
            let inner = self.inner.as_ref();
            let count = inner.ref_count.get();
            if count == 1 {
                drop(Box::from_raw(self.inner.as_ptr()));
            } else {
                inner.ref_count.set(count - 1);
            }
        }
    }
}
 
fn main() {
    let rc1 = SimpleRc::new(42);
    println!("Value: {}, refs: {}", rc1.get(), rc1.ref_count());
    
    let rc2 = rc1.clone();
    println!("Value: {}, refs: {}", rc1.get(), rc1.ref_count());
    
    drop(rc2);
    println!("After drop, refs: {}", rc1.ref_count());
}

Cell vs RefCell

use std::cell::{Cell, RefCell};
 
fn main() {
    // Cell: Only for Copy types
    let cell: Cell<i32> = Cell::new(42);
    cell.set(100);
    let value = cell.get();
    
    // RefCell: For any type, but with runtime borrow checking
    let refcell: RefCell<String> = RefCell::new(String::from("hello"));
    refcell.borrow_mut().push_str(" world");
    
    // Cell cannot hold non-Copy types:
    // let bad: Cell<String> = Cell::new(String::from("test")); // Error!
    
    // RefCell can hold any type:
    let good: RefCell<Vec<i32>> = RefCell::new(vec![1, 2, 3]);
    good.borrow_mut().push(4);
    
    println!("Cell: {}", value);
    println!("RefCell: {}", refcell.borrow());
    println!("RefCell Vec: {:?}", good.borrow());
}

Cell for Immutable API Design

use std::cell::Cell;
 
pub struct Config {
    debug: Cell<bool>,
    log_level: Cell<u32>,
}
 
impl Config {
    pub fn new() -> Self {
        Self {
            debug: Cell::new(false),
            log_level: Cell::new(1),
        }
    }
    
    // Immutable API that internally mutates
    pub fn enable_debug(&self) {
        self.debug.set(true);
    }
    
    pub fn set_log_level(&self, level: u32) {
        self.log_level.set(level);
    }
    
    pub fn is_debug(&self) -> bool {
        self.debug.get()
    }
    
    pub fn log_level(&self) -> u32 {
        self.log_level.get()
    }
}
 
fn main() {
    let config = Config::new();
    
    // Can modify through shared reference
    let config_ref = &config;
    config_ref.enable_debug();
    config_ref.set_log_level(3);
    
    println!("Debug: {}, Level: {}", config.is_debug(), config.log_level());
}

Cell with Interior State Machine

use std::cell::Cell;
 
#[derive(Clone, Copy, PartialEq)]
enum ParserState {
    Start,
    InWord,
    InNumber,
    End,
}
 
struct Parser {
    state: Cell<ParserState>,
    position: Cell<usize>,
}
 
impl Parser {
    fn new() -> Self {
        Self {
            state: Cell::new(ParserState::Start),
            position: Cell::new(0),
        }
    }
    
    fn advance(&self, input: &[char]) -> Option<char> {
        let pos = self.position.get();
        if pos < input.len() {
            let ch = input[pos];
            self.position.set(pos + 1);
            self.update_state(ch);
            Some(ch)
        } else {
            self.state.set(ParserState::End);
            None
        }
    }
    
    fn update_state(&self, ch: char) {
        let new_state = match (self.state.get(), ch) {
            (ParserState::Start, c) if c.is_alphabetic() => ParserState::InWord,
            (ParserState::Start, c) if c.is_numeric() => ParserState::InNumber,
            (ParserState::InWord, c) if c.is_alphabetic() => ParserState::InWord,
            (ParserState::InNumber, c) if c.is_numeric() => ParserState::InNumber,
            _ => ParserState::Start,
        };
        self.state.set(new_state);
    }
    
    fn state(&self) -> ParserState {
        self.state.get()
    }
}
 
fn main() {
    let parser = Parser::new();
    let input = ['h', 'e', 'l', 'l', 'o', ' ', '1', '2', '3'];
    
    while let Some(ch) = parser.advance(&input) {
        println!("Char: {}, State: {:?}", ch, parser.state());
    }
}

Cell Performance Characteristics

use std::cell::Cell;
 
fn main() {
    let cell = Cell::new(0u32);
    
    // Cell operations are very cheap - just memory reads/writes
    // No runtime borrow checking overhead
    
    for i in 0..1000 {
        cell.set(i);
        let _ = cell.get();
    }
    
    println!("Final value: {}", cell.get());
    
    // Cell<T> has the same size as T
    println!("Size of Cell<u32>: {}", std::mem::size_of::<Cell<u32>>());
    println!("Size of u32: {}", std::mem::size_of::<u32>());
}

Summary

Cell Methods:

Method Description
new(value) Create a new Cell
get() Get a copy of the value
set(value) Set a new value
replace(value) Replace and return old value
swap(&other) Swap with another Cell
take() Replace with default and return old value
into_inner() Consume and return inner value
as_ptr() Get raw pointer to inner value

Cell vs Other Interior Mutability Types:

Type Works With Thread Safe Overhead
Cell<T> Copy types only No (!Sync) None
RefCell<T> Any type No (!Sync) Runtime borrow check
Mutex<T> Any type Yes Lock overhead
AtomicXxx Specific numeric types Yes Atomic ops

When to Use Cell:

Scenario Appropriate?
Small Copy types āœ… Yes
Counters, flags āœ… Yes
State machines āœ… Yes
Caching āœ… Yes
Non-Copy types āŒ Use RefCell
Thread-safe mutation āŒ Use Mutex or atomics
Need &T or &mut T āŒ Use RefCell

Key Points:

  • Cell<T> provides interior mutability for Copy types
  • Values are moved in and out, never borrowed directly
  • No runtime borrow checking overhead
  • Single-threaded only (!Sync)
  • Zero-cost abstraction (same size as T)
  • Use RefCell for non-Copy types or when you need references
  • Use Mutex or atomics for thread-safe mutation
  • Perfect for simple state, counters, and flags