Skip to content

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: 2
bob strong count: 2
end of main
▶ Try it Yourself

Notice 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 bob
alice: strong = 1, weak = 1
end of main
dropping bob
dropping alice
▶ Try it Yourself

This 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 returns None. It’s a handy starting value.
  • Rc::strong_count counts owners. Rc::weak_count counts weak links. Only the strong count decides when the value is dropped.
  • The Rc you get back from upgrade() 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 home
full path: home/photos/trips
home: strong = 1, weak = 1
photos: strong = 2, weak = 1
▶ Try it Yourself

Read 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 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 10
logger got: score is 10
screen got: score is 20
(a listener is gone, skipping)
listeners left: 1
▶ Try it Yourself

The 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 new String”.
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)
▶ Try it Yourself

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.

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: 3
starts with e: true
Welcome, eve!
saved "eve" to the user list
▶ Try it Yourself

Cow 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.

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.

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` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Try it Yourself

The 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"),
}

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.

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 post
▶ Try it Yourself

The comment never keeps the post alive. Once the only Rc is dropped, upgrade() returns None and the comment handles it calmly instead of panicking.

Write fn escape(text: &str) -> Cow<'_, str> that replaces every < with &lt;. 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('<', "&lt;") for the owned case.

Show solution
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> {
if text.contains('<') {
Cow::Owned(text.replace('<', "&lt;"))
} 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 &lt; 2 (owned)
▶ Try it Yourself

Only the text that actually contains < pays for a new String. Everything else goes straight through, untouched.

  • Two Rc values that point at each other never reach a strong count of 0, so they leak and their Drop never runs.
  • Rc::downgrade makes a Weak<T>, which doesn’t keep the value alive. upgrade() gives back Option<Rc<T>>.
  • In trees, children are owned with Rc and parent links are Weak. Ownership flows down, weak links point up.
  • Observer lists and caches can hold Weak so they never keep things alive by accident.
  • Cow<'_, str> returns borrowed text when nothing changed and an owned String only when it did. It derefs to &str, and .into_owned() gives you a String.

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