When &self isn't enough
You have a struct. You pass it around as an immutable reference because the API demands &self. Halfway through a method, you realize you need to update a counter inside the struct. Or toggle a flag. Or record a timestamp. The borrow checker blocks you. You can't change the method signature to &mut self because the trait you're implementing requires &self, or because multiple parts of your code hold references to the struct simultaneously.
You're stuck. Or you think you are.
Rust provides interior mutability to solve this. RefCell<T> is the famous tool, but it comes with a runtime cost: it tracks borrows and can panic if you violate the rules. There's a lighter tool for a specific job. Cell<T> lets you mutate data through an immutable reference with zero runtime overhead. It has one strict requirement: the inner type must implement Copy.
Cell is interior mutability for Copy types
Cell<T> wraps a value and allows you to read and write it via &self. It doesn't give you references to the inner value. You can't get a &T or a &mut T out of a Cell. You get the value itself.
Think of a Cell like a digital counter on a wall. You can look at the number (get the value). You can press a button to change the number (set the value). You never take the number home. You never hold a reference to the number. You interact by value. The wall holds the state, and you swap values in and out.
This design eliminates the need for borrow tracking. Since you can't hold a reference to the inner data, you can never create aliasing mutable references. The compiler knows you're safe. Cell is just a wrapper that uses unsafe internally to bypass the borrow checker, but the public API is safe because the value semantics prevent data races and aliasing.
use std::cell::Cell;
struct ClickCounter {
// Cell<u32> stores a u32. u32 implements Copy, so this compiles.
// Cell allows mutation through &self.
count: Cell<u32>,
}
impl ClickCounter {
fn new() -> Self {
ClickCounter {
// Cell::new initializes the inner value.
count: Cell::new(0),
}
}
// &self allows mutation of count inside the method.
fn click(&self) {
// get() copies the value out of the cell.
// No reference is returned, so no borrow is created.
let current = self.count.get();
// set() copies the new value into the cell.
// The old value is dropped.
self.count.set(current + 1);
}
fn get_count(&self) -> u32 {
self.count.get()
}
}
fn main() {
let counter = ClickCounter::new();
// We can call click() multiple times with &self.
// No mutable borrow needed.
counter.click();
counter.click();
println!("Count: {}", counter.get_count()); // Count: 2
}
Cell is the zero-cost toggle. Use it for flags and counters, and keep your performance tight.
How Cell works under the hood
When you call get(), Cell copies the value from the heap or stack location it manages and returns it to you. When you call set(), Cell copies the new value in and drops the old one.
Because T must implement Copy, the copy operation is trivial. It's just a bitwise copy of the bytes. There's no allocation, no reference counting, no borrow checking. Cell<T> has the exact same performance as a plain field if you could mutate it directly. The overhead is zero.
This is why Cell is perfect for simple types. Booleans, integers, floats, pointers, and small structs that implement Copy. If you need to mutate a String or a Vec, Cell won't work. Those types don't implement Copy, and for good reason.
Why the Copy bound?
The Copy bound isn't an arbitrary restriction. It's a safety guarantee.
Cell must always contain a valid value. You can't have a Cell that is "empty". If get() moved the value out, the Cell would be left with a hole. The next get() would read garbage. Rust prevents this by requiring T: Copy.
If T is Copy, get() doesn't move the value. It copies it. The original value stays in the Cell. The Cell remains valid. set() replaces the value, but it takes ownership of the new value, so the Cell is never empty.
Contrast this with RefCell<T>. RefCell returns a Ref<T> or RefMut<T>, which are guard objects that borrow the inner value. The value stays in the RefCell, and the guard ensures the borrow ends when the guard is dropped. RefCell can hold non-Copy types because it never moves the value out. It only borrows.
Cell can't borrow. It returns values. So it needs Copy to avoid moving.
Trust the Copy bound. It's your safety net. If T doesn't copy, Cell won't hold it.
Realistic example: The dirty flag
A common pattern in Rust is the "dirty flag". You have a configuration object or a cache. You want to track whether the data has been modified since the last save. You pass the object around as &self to various handlers. You need to mark it as dirty when a handler makes a change.
Cell<bool> is the ideal tool here. It's a single byte, zero overhead, and works through &self.
use std::cell::Cell;
struct Config {
name: String,
// dirty flag tracks if config has changed.
// Cell<bool> allows mutation through &self.
dirty: Cell<bool>,
}
impl Config {
fn new(name: &str) -> Self {
Config {
name: name.to_string(),
dirty: Cell::new(false),
}
}
// This method takes &self.
// We can't mutate self.name directly because String is not Copy.
// But we can mark the config as dirty.
fn update_name(&self, new_name: &str) {
// In a real app, you'd need RefCell<String> for the name,
// or restructure to pass &mut self.
// Here, we just show the dirty flag pattern.
self.dirty.set(true);
}
fn is_dirty(&self) -> bool {
self.dirty.get()
}
fn save(&self) {
// replace() is a handy method on Cell.
// It sets the new value and returns the old value.
// This is atomic-like: you swap and get the previous state.
let was_dirty = self.dirty.replace(false);
if was_dirty {
println!("Saving config...");
// Perform save logic here.
} else {
println!("Config is clean. No save needed.");
}
}
}
fn main() {
let config = Config::new("MyApp");
config.update_name("NewApp");
assert!(config.is_dirty());
config.save(); // Saves and clears dirty flag.
assert!(!config.is_dirty());
config.save(); // No save needed.
}
replace() is a convention worth knowing. It swaps the value and returns the old one in one step. Use it when you need to reset a flag and check the previous state atomically.
Pitfalls and compiler errors
Cell is simple, but it has sharp edges.
Cell only works for Copy types.
If you try to put a Vec or String in a Cell, the compiler rejects you.
use std::cell::Cell;
struct Bad {
// Vec<String> does not implement Copy.
data: Cell<Vec<String>>,
}
The compiler emits E0277 (the trait bound Copy is not satisfied). The error message tells you exactly what's wrong: Vec<String> doesn't implement Copy. You can't use Cell here. Reach for RefCell instead.
Cell doesn't have borrow methods.
Cell and RefCell have different APIs. Cell has get() and set(). RefCell has borrow() and borrow_mut(). If you try to call borrow_mut() on a Cell, you get E0599 (no method named borrow_mut).
let cell = Cell::new(5);
cell.borrow_mut(); // Error: E0599
Cell is not thread-safe.
Cell<T> is !Sync by default. You can't share a Cell across threads. If you try to put a Cell inside an Arc, the compiler blocks you with E0277 because Arc<T> requires T: Sync.
use std::cell::Cell;
use std::sync::Arc;
let arc_cell = Arc::new(Cell::new(0)); // Error: E0277
Cell is for single-threaded interior mutability. If you need thread-safe interior mutability, use Mutex<T> or AtomicU32 for simple counters.
Don't force Cell into Arc. It's single-threaded by design.
Decision: Cell vs RefCell vs Mutex
Choosing the right interior mutability tool depends on your type and your threading model.
Use Cell<T> when T: Copy and you need interior mutability with zero runtime overhead. Cell is perfect for flags, counters, and small values. It's faster than RefCell because there's no borrow tracking.
Use RefCell<T> when T is not Copy and you need interior mutability in a single-threaded context. RefCell allows you to get mutable references to complex types like Vec or String. It checks borrows at runtime and panics if you violate the rules.
Use Mutex<T> when you need interior mutability across threads. Mutex provides thread-safe access to the inner value. It blocks other threads while you hold the lock. It works for any type, Copy or not.
Use &mut T when you can restructure your code to pass mutable references. Interior mutability is a workaround. If you can design your API to use explicit mutable borrows, do it. It's clearer and more idiomatic.
Use AtomicU32 or other atomics when you need thread-safe mutation of simple numeric types without locking. Atomics are faster than Mutex for counters and flags, but they only support specific operations like fetch_add or compare_exchange.