π¦ Rust Borrow Checker β Catches Real Bugs
The borrow checker isn't just a compiler pedant β it prevents the C++ bugs that wake you at 3 AM. Iterator invalidation, use-after-free, and data races, caught at compile time.
π― The Three Rules
Rustβs ownership system reduces to three rules:
- Each value has exactly one owner at any time
- References must never outlive the data they point to
- At any moment, you have either one mutable reference or any number of immutable references (never both)
These rules are enforced entirely at compile time. No runtime overhead, no garbage collector, no null pointer checks.
// Rule 1: One owner
let s = String::from("hello");
let t = s; // s moves to t
// println!("{s}"); // β Compile error! s is no longer valid
// Rule 3: XOR of mutability
let mut v = Vec::new();
let r1 = &v;
let r2 = &v;
// let r3 = &mut v; // β Cannot borrow as mutable while immutable refs exist
println!("{r1} {r2}"); // β
Immutable references coexist fine
π Bug 1: Iterator Invalidation
The C++ Version (Compiles, Crashes at 3AM)
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0) {
v.push_back(*it * 10); // Mutates v while iterating!
}
}
// Undefined behavior: iterator may be invalidated
return 0;
}
This compiles with zero warnings at -Wall -Wextra. What happens at runtime?
push_backmay trigger reallocation when capacity is exceeded- After reallocation,
itpoints to freed memory - The comparison
it != v.end()and the increment++itoperate on dangling pointers - Demonstrably, this may: segfault, corrupt memory, silently produce wrong results, or work by coincidence until the vector grows past its initial capacity
The Rust Version (Wonβt Compile)
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
for item in &v { // Immutable borrow of v
if *item % 2 == 0 {
v.push(*item * 10); // β Compile error!
}
} // cannot borrow `v` as mutable
} // because it is also borrowed as immutable
The Rust compiler catches this because the for loop holds an immutable reference to v, and push requires a mutable reference. Rule 3 says these cannot coexist.
The error message is helpful:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:5:13
|
3 | for item in &v { // immutable borrow occurs here
| --
4 | if *item % 2 == 0 {
5 | v.push(*item * 10); // mutable borrow occurs here
| ^^^^^^^^^^^^^^^^^^
6 | }
7 | } // immutable borrow ends here
The compiler even tells you where the immutable borrow starts and ends β no guessing, no debugger attached.
π Bug 2: Use-After-Free
The C++ Version
int* dangling_ptr() {
int x = 42;
return &x; // Returns pointer to stack-local that will be destroyed
}
int main() {
int* p = dangling_ptr();
std::cout << *p << std::endl; // Maybe prints 42, maybe crashes
return 0;
}
With Clang/GCC at -O2, this compiles without warning (unless you specifically enable -Wreturn-local-addr on some versions, but not universally). The pointer p dangles the moment dangling_ptr returns.
The Rust Version
fn dangling_ref() -> &i32 {
let x = 42;
&x // β Compile error!
}
fn main() {
let p = dangling_ref();
println!("{p}");
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:23
|
1 | fn dangling_ref() -> &i32 {
| ^ expected named lifetime parameter
|
help: consider using the `'static` lifetime
|
1 | fn dangling_ref() -> &'static i32 {
Even the notation &i32 without a lifetime is caught immediately. Adding 'static wonβt help β there is no &'static i32 to return from a local.
π Bug 3: Double-Free
The C++ Version
class Buffer {
int* data;
public:
Buffer(size_t n) : data(new int[n]) {}
~Buffer() { delete[] data; }
// No copy constructor or assignment operator!
};
int main() {
Buffer a(100);
Buffer b = a; // Shallow copy! Both a and b point to same memory
return 0; // Double free: both destructors call delete[] on same address
}
The Rule of Three (or Five in C++11) is easy to forget, and the compiler does not enforce it. The double-free manifests as a heap corruption crash β often in an unrelated malloc/free called hours later.
The Rust Version
struct Buffer {
data: Vec<i32>,
}
fn main() {
let a = Buffer { data: vec![0; 100] };
let b = a; // Moves ownership from a to b
// println!("{}", a.data.len()); // β Compile error: a is moved
} // Only b's destructor runs β clean!
Rustβs move semantics make the βforgot the copy constructorβ bug impossible: let b = a moves ownership, not shallow copies. You must explicitly opt into cloning:
let b = a.clone(); // β
Explicit shallow copy (data is reference-counted)
And Vecβs Clone implementation does a deep copy, so thereβs no shared memory to double-free.
π₯ Real Scenario: Concurrent Cache
Here is a more realistic example that Rust catches at compile time while C++ ships.
The problem: A shared cache with concurrent readers and a periodic writer. The writer holds a lock that invalidates an internal index while a reader is mid-iteration.
C++ (UB ships to production)
#include <unordered_map>
#include <shared_mutex>
#include <thread>
#include <vector>
class ConcurrentCache {
std::unordered_map<int, std::string> cache;
mutable std::shared_mutex mtx;
public:
void insert(int k, std::string v) {
std::unique_lock lock(mtx); // Exclusive lock
cache[k] = std::move(v);
}
std::vector<std::string> match_prefix(const std::string& prefix) {
std::shared_lock lock(mtx); // Shared lock (reader)
std::vector<std::string> results;
// Mutates cache while iterating β UB!
for (const auto& [k, v] : cache) {
if (v.starts_with(prefix)) {
results.push_back(v);
// If we also prune expired entries:
// cache.erase(k); β Iterator invalidation (hidden in real code)
}
}
return results;
}
};
The erase inside the loop invalidates the iterator β UB. In a large codebase, the prune logic may be buried behind a helper function, or added months later by another developer who doesnβt realize the caller is iterating.
Rust (Compile-time guarantee)
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
struct ConcurrentCache {
cache: RwLock<HashMap<i32, String>>,
}
impl ConcurrentCache {
fn insert(&self, k: i32, v: String) {
let mut cache = self.cache.write().unwrap();
cache.insert(k, v);
}
fn match_prefix(&self, prefix: &str) -> Vec<String> {
let cache = self.cache.read().unwrap(); // Read lock
// β Won't compile β cache is an immutable reference
// cache.insert(0, "foo".into());
// β
Iterating is safe because cache is immutably borrowed
cache.iter()
.filter(|(_, v)| v.starts_with(prefix))
.map(|(_, v)| v.clone())
.collect()
}
}
The RwLock::read() returns a RwLockReadGuard that derefs to &HashMap. Calling insert would require &mut HashMap β which the compiler forbids. The mutation can never happen.
And HashMap::iter() borrows the map immutably for as long as the iterator lives, so even without the RwLock, this code wonβt compile:
let mut map = HashMap::new();
for (k, v) in &map { // Immutable borrow begins
map.insert(0, "x".into()); // β Mutable borrow while immutable exists
} // Immutable borrow ends
βοΈ The Tradeoff
The borrow checker is Rustβs most controversial feature β and its most valuable.
The Pain
Per the Rust Foundation 2025 Survey:
| Statistic | Value |
|---|---|
| Developers citing borrow checker as top frustration | 71% |
| Developers who later value it most | 89% |
| Average time to feel productive in Rust | 3β6 months |
Practical Strategies
Use Rc/Arc for shared ownership when no single owner is natural:
use std::rc::Rc;
struct Node {
value: i32,
children: Vec<Rc<Node>>, // Multiple nodes can share children
}
let leaf = Rc::new(Node { value: 1, children: vec![] });
let branch = Rc::new(Node {
value: 2,
children: vec![Rc::clone(&leaf), Rc::clone(&leaf)], // Reference count: 2
});
Use RefCell for interior mutability when you need runtime borrow checking:
use std::cell::RefCell;
struct Cache {
inner: RefCell<HashMap<String, Vec<u8>>>,
}
impl Cache {
fn get_or_compute(&self, key: &str) -> Vec<u8> {
// Immutable self β fine
if let Some(val) = self.inner.borrow().get(key) {
return val.clone();
}
// Runtime mutable borrow (panics if already borrowed)
let computed = compute(key);
self.inner.borrow_mut().insert(key.to_string(), computed.clone());
computed
}
}
Restructure to avoid overlapping borrows β the most common fix is separating concerns:
// β Problematic: trying to hold two references from one struct
struct Viewer {
data: Vec<i32>,
cursor: usize,
}
impl Viewer {
fn advance_bad(&mut self) {
let data = &self.data; // Immutable borrow
let val = data[self.cursor];
self.cursor += 1; // β Mutable borrow
}
// β
Solution: split into separate borrows
fn advance_good(data: &[i32], cursor: &mut usize) {
let val = data[*cursor];
*cursor += 1;
}
}
π Real-World Impact
| Bug Class | C++ Detection | Rust Detection | Frequency in C++ Codebases |
|---|---|---|---|
| Iterator invalidation | Runtime (UB) | Compile time | 15β20% of all CVEs in memory-unsafe code |
| Use-after-free | Runtime (UB) | Compile time | ~30% of Chrome security bugs (per Google) |
| Double-free | Runtime (UB) | Compile time | ~10% of Firefox security bugs |
| Data race | Heuristic tools (TSan) | Compile time (Send/Sync) | 40β70% of concurrency bugs |
Google has reported a 70% reduction in security bugs in Androidβs Rust code vs equivalent C++ code. Microsoft reports that ~70% of their CVEs are memory safety bugs that Rust would prevent.
π Final Thought
The borrow checker is not the compiler being difficult β it is the compiler being honest. Every borrow-check error is a bug that would have been a segfault, a security advisory, or a βworks on my machineβ defect in C++.
The learning curve is real, but the payoff is equally real: Rust code that, once it compiles, is memory-safe and data-race-free by construction.
π Further Reading
| Resource | Link |
|---|---|
| The Rust Book β Ownership | doc.rust-lang.org/book/ch04-00-understanding-ownership.html |
| Rustonomicon β Unsafe Rust | doc.rust-lang.org/nomicon/ |
| Rust 2025 Survey Results | blog.rust-lang.org/2025/02/15/2025-survey-results.html |
| Google Rust Adoption Report | security.googleblog.com/2024/12/memory-safety-in-android-ecosystem.html |
| CrUX: C++ vs Rust Safety Stats | chadaustin.me/2023/08/rust-vs-cpp-safety-stats/ |