Rust Weak & Cow
On the Smart Pointers page you met Box, Rc and RefCell. This page adds two more tools that fix two very real problems. Weak<T> lets you point at a value without keeping it alive. Cow<T> lets a function hand back borrowed data when it can, and only make a copy when it has to.
Think of Rc as a house key: as long as somebody holds a key, the house stays. Weak is more like the address written on a piece of paper. You can use it to find the house, but if everyone gives their keys back and the house is gone, the address just leads nowhere. Nobody is stuck paying for a house they forgot about.
The problem: Rc cycles never get cleaned up
Section titled “The problem: Rc cycles never get cleaned up”An Rc value is dropped when its strong count reaches 0. That works great until two values hold an Rc to each other. Then each one keeps the other alive, forever.
To see this, the Person type below implements Drop, so it prints a message when it’s cleaned up. Watch which messages appear:
use std::cell::RefCell;use std::rc::Rc;
struct Person { name: String, friend: RefCell<Option<Rc<Person>>>,}
impl Drop for Person { fn drop(&mut self) { println!("dropping {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// alice points at bob, and bob points back at alice *alice.friend.borrow_mut() = Some(Rc::clone(&bob)); *bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("alice strong count: {}", Rc::strong_count(&alice)); println!("bob strong count: {}", Rc::strong_count(&bob)); println!("end of main");}Output:
alice strong count: 2bob strong count: 2end of mainNotice what’s missing: there is no dropping alice and no dropping bob. When main ends, the variables alice and bob go away, so each count drops from 2 to 1. But it never reaches 0, because each person is still held by the other’s friend field. That memory is leaked.
Don’t feel bad if this surprises you. The compiler can’t catch it, and it isn’t a crash. It’s just memory that never comes back. In a long-running program that keeps building cycles, it adds up.
Weak<T>: a pointer that doesn’t keep things alive
Section titled “Weak<T>: a pointer that doesn’t keep things alive”The fix is to make one or both links weak. You get a Weak from an Rc with Rc::downgrade. A weak link doesn’t count as an owner, so it doesn’t stop the value from being dropped.
Because the value might already be gone, you can’t use a Weak directly. You call .upgrade(), which gives you an Option<Rc<T>>: Some if the value is still alive, None if it isn’t.
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Person { name: String, friend: RefCell<Weak<Person>>,}
impl Drop for Person { fn drop(&mut self) { println!("dropping {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob); *bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() { println!("alice's friend is {}", friend.name); }
println!("alice: strong = {}, weak = {}", Rc::strong_count(&alice), Rc::weak_count(&alice)); println!("end of main");}Output:
alice's friend is bobalice: strong = 1, weak = 1end of maindropping bobdropping aliceThis time both dropping lines appear. Alice has one strong owner (the alice variable) and one weak link (from Bob). When main ends, the strong count reaches 0, and Rust cleans her up. Bob goes first because variables are dropped in reverse order of creation.
A few things to remember:
Weak::new()makes an empty weak pointer that points at nothing.upgrade()on it always returnsNone. It’s a handy starting value.Rc::strong_countcounts owners.Rc::weak_countcounts weak links. Only the strong count decides when the value is dropped.- The
Rcyou get back fromupgrade()is a real, temporary owner. When it goes out of scope, the strong count goes back down.
A folder tree: children own, parents are weak
Section titled “A folder tree: children own, parents are weak”Trees are the classic place for Weak. A folder owns its subfolders, so children holds Rc. A subfolder also wants to know its parent, but it shouldn’t own it. If both directions used Rc, you’d have a cycle again. So parent is a Weak:
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Folder { name: String, parent: RefCell<Weak<Folder>>, children: RefCell<Vec<Rc<Folder>>>,}
fn new_folder(name: &str) -> Rc<Folder> { Rc::new(Folder { name: name.to_string(), parent: RefCell::new(Weak::new()), children: RefCell::new(Vec::new()), })}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) { *child.parent.borrow_mut() = Rc::downgrade(parent); parent.children.borrow_mut().push(child);}
fn full_path(folder: &Folder) -> String { match folder.parent.borrow().upgrade() { Some(parent) => format!("{}/{}", full_path(&parent), folder.name), None => folder.name.clone(), }}
fn main() { let home = new_folder("home"); let photos = new_folder("photos"); let trips = new_folder("trips");
add_child(&home, Rc::clone(&photos)); add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() { Some(parent) => println!("photos lives in {}", parent.name), None => println!("photos has no parent"), }
println!("full path: {}", full_path(&trips)); println!("home: strong = {}, weak = {}", Rc::strong_count(&home), Rc::weak_count(&home)); println!("photos: strong = {}, weak = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));}Output:
photos lives in homefull path: home/photos/tripshome: strong = 1, weak = 1photos: strong = 2, weak = 1Read the counts slowly. photos has two strong owners: the photos variable and the children list inside home. It has one weak link: the parent field of trips. home has only one strong owner, its own variable, because nobody owns their parent. That’s exactly the shape you want: ownership flows down the tree, and the links going up are weak.
RefCell is there because you set parent and push to children after the folders are already inside an Rc, and Rc alone only gives you shared, read-only access.
Weak lists that don’t keep things alive
Section titled “Weak lists that don’t keep things alive”Weak is also great for “keep an eye on these, but don’t hold onto them” lists: listeners, observers, or a cache of things someone else owns. Here a game score sends updates to whoever is still listening. When a listener is dropped, its weak link simply stops working:
use std::rc::{Rc, Weak};
struct Listener { name: String,}
fn notify(listeners: &[Weak<Listener>], event: &str) { for weak in listeners { match weak.upgrade() { Some(listener) => println!("{} got: {event}", listener.name), None => println!("(a listener is gone, skipping)"), } }}
fn main() { let screen = Rc::new(Listener { name: String::from("screen") }); let logger = Rc::new(Listener { name: String::from("logger") });
let mut listeners: Vec<Weak<Listener>> = Vec::new(); listeners.push(Rc::downgrade(&screen)); listeners.push(Rc::downgrade(&logger));
notify(&listeners, "score is 10");
drop(logger); // the logger is closed by its owner
notify(&listeners, "score is 20");
// tidy up links that point at nothing listeners.retain(|weak| weak.upgrade().is_some()); println!("listeners left: {}", listeners.len());}Output:
screen got: score is 10logger got: score is 10screen got: score is 20(a listener is gone, skipping)listeners left: 1The list never stopped the logger from being dropped. If it had held Rc instead, the logger would have lived on inside the list, still getting messages nobody wanted. With Weak, the owner decides when something goes away, and everyone else just checks with upgrade().
Cow: borrow when you can, copy when you must
Section titled “Cow: borrow when you can, copy when you must”Now a different problem. Say you write a function that cleans up usernames. Most names are already fine. A few have extra spaces or capital letters. If your function always returns a new String, you make a copy of every single name, even the ones you didn’t change.
Cow (“clone on write”, from std::borrow::Cow) solves this. A Cow<'_, str> is an enum with two variants:
Cow::Borrowed(&str): “here’s the original text, untouched”. No allocation.Cow::Owned(String): “I had to change it, so here’s a newString”.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper { Cow::Borrowed(name) // already clean: hand back the original } else { Cow::Owned(trimmed.to_lowercase()) // needs fixing: build a new String }}
fn main() { for input in ["alice", " Bob ", "CAROL", "dave"] { match normalize(input) { Cow::Borrowed(name) => println!("{input:?} -> {name:?} (borrowed, no copy)"), Cow::Owned(name) => println!("{input:?} -> {name:?} (owned, new String)"), } }}Output:
"alice" -> "alice" (borrowed, no copy)" Bob " -> "bob" (owned, new String)"CAROL" -> "carol" (owned, new String)"dave" -> "dave" (borrowed, no copy)Two of the four names cost nothing at all. In a real program where 99% of the input is already clean, that’s a lot of copies you never make. The '_ in Cow<'_, str> just says “this may borrow from the input”. If lifetimes still feel fuzzy, the Lifetimes page explains them.
Using a Cow like a normal string
Section titled “Using a Cow like a normal string”You usually don’t need to match on a Cow at all. It derefs to &str, so you can call string methods on it and pass &name to any function that takes &str. When you really need a String you own, call .into_owned(). It moves the String out if there is one, and only copies if the Cow was borrowed.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) { Cow::Borrowed(name) } else { Cow::Owned(trimmed.to_lowercase()) }}
fn greet(name: &str) { println!("Welcome, {name}!");}
fn main() { let name = normalize(" Eve ");
println!("length: {}", name.len()); // str methods work directly println!("starts with e: {}", name.starts_with('e')); greet(&name); // &Cow<str> turns into &str
let saved: String = name.into_owned(); // now it's a plain String println!("saved {saved:?} to the user list");}Output:
length: 3starts with e: trueWelcome, eve!saved "eve" to the user listCow works with other types too, like Cow<'_, [i32]> for a slice that might need changing. For text, though, Cow<'_, str> is the one you’ll see most.
When to use what
Section titled “When to use what”Here’s a short cheat sheet for all the pointer-ish types you’ve met so far:
| Type | What it gives you | Reach for it when |
|---|---|---|
Box<T> |
One owner, value on the heap | Recursive types, or a big value you want to move cheaply |
Rc<T> |
Many owners, one thread | Several parts of your program share the same data |
Arc<T> |
Many owners, across threads | Same as Rc, but shared between threads (see Shared State) |
Weak<T> |
A link that doesn’t own | Parent links, observer lists, caches, breaking Rc cycles |
RefCell<T> |
Mutation through a shared reference, checked at runtime | You need to change something that lives inside an Rc |
Cow<'_, T> |
Borrowed or owned, decided at runtime | A function that usually returns its input unchanged |
If you’re not sure, start with plain ownership and borrowing. Add these only when the compiler, or a memory leak, tells you that you need them.
Common mistakes
Section titled “Common mistakes”Unwrapping an upgrade after the value is gone
Section titled “Unwrapping an upgrade after the value is gone”upgrade() returns an Option for a reason. If you .unwrap() it out of habit, your program will panic the moment the value has been dropped:
use std::rc::Rc;
fn main() { let session = Rc::new(String::from("user-42")); let link = Rc::downgrade(&session);
drop(session); // the session ends
let current = link.upgrade().unwrap(); println!("still logged in as {current}");}When you run it, you get:
thread 'main' (12345) panicked at src/main.rs:9:34:called `Option::unwrap()` on a `None` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceThe fix is to treat “it’s gone” as a normal case, with match or if let:
match link.upgrade() { Some(current) => println!("still logged in as {current}"), None => println!("session has ended"),}Using Rc for the link that points back
Section titled “Using Rc for the link that points back”If a child holds Rc<Parent> and the parent holds Rc<Child>, you’ve built the cycle from the first section. Nothing crashes and nothing warns you; the memory just never comes back. A good rule: decide who owns whom, use Rc in that direction, and Weak for every link going the other way.
struct Folder { parent: RefCell<Rc<Folder>>, // wrong: child keeps parent alive children: RefCell<Vec<Rc<Folder>>>,}Change parent to RefCell<Weak<Folder>> and the cycle is gone.
Practice
Section titled “Practice”1. Comments on a deleted post
Section titled “1. Comments on a deleted post”Make a Post with a title, kept in an Rc. Make a Comment with a text and a post: Weak<Post>. Write a function describe(comment: &Comment) that prints "<text>" on <title> if the post still exists, or "<text>" on a deleted post if not. Call it, drop the post, and call it again. Hint: use match comment.post.upgrade().
Show solution
use std::rc::{Rc, Weak};
struct Post { title: String,}
struct Comment { text: String, post: Weak<Post>,}
fn describe(comment: &Comment) { match comment.post.upgrade() { Some(post) => println!("\"{}\" on {}", comment.text, post.title), None => println!("\"{}\" on a deleted post", comment.text), }}
fn main() { let post = Rc::new(Post { title: String::from("Learning Rust") }); let comment = Comment { text: String::from("Great post!"), post: Rc::downgrade(&post) };
describe(&comment); drop(post); describe(&comment);}Output:
"Great post!" on Learning Rust"Great post!" on a deleted postThe comment never keeps the post alive. Once the only Rc is dropped, upgrade() returns None and the comment handles it calmly instead of panicking.
2. Escaping text only when needed
Section titled “2. Escaping text only when needed”Write fn escape(text: &str) -> Cow<'_, str> that replaces every < with <. If the text has no <, return it borrowed. Test it on "hello world" and "1 < 2", and print which variant you got. Hint: check with text.contains('<') first, then use text.replace('<', "<") for the owned case.
Show solution
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> { if text.contains('<') { Cow::Owned(text.replace('<', "<")) } else { Cow::Borrowed(text) }}
fn main() { for text in ["hello world", "1 < 2"] { match escape(text) { Cow::Borrowed(s) => println!("{s} (borrowed)"), Cow::Owned(s) => println!("{s} (owned)"), } }}Output:
hello world (borrowed)1 < 2 (owned)Only the text that actually contains < pays for a new String. Everything else goes straight through, untouched.
- Two
Rcvalues that point at each other never reach a strong count of 0, so they leak and theirDropnever runs. Rc::downgrademakes aWeak<T>, which doesn’t keep the value alive.upgrade()gives backOption<Rc<T>>.- In trees, children are owned with
Rcand parent links areWeak. Ownership flows down, weak links point up. - Observer lists and caches can hold
Weakso they never keep things alive by accident. Cow<'_, str>returns borrowed text when nothing changed and an ownedStringonly when it did. It derefs to&str, and.into_owned()gives you aString.
Quick check
Section titled “Quick check”Quick check
1. Two values each hold an Rc to the other. What happens when main ends?
2. What does weak.upgrade() return?
3. In a folder tree, which link should be Weak?
4. When does a Cow<'_, str> function avoid making a new String?
Score: 0 / 4
Read more
Section titled “Read more”- Reference Cycles Can Leak Memory — The Rust Book: the official walk-through of Rc cycles and a tree with
Weakparent links. std::rc::Weak— standard library docs: every method onWeak, includingupgrade,strong_countandWeak::new.std::rc— standard library docs: how strong and weak counts work together, with a longer example of breaking cycles.std::borrow::Cow— standard library docs: theCowenum,to_mut,into_owned, and examples beyond strings.