Cow: Clone-on-Write
Reviewed & published by Brayan K
Cow (Clone on Write) is a smart enum that holds either borrowed or owned data, letting a function work with a reference for free and allocate a new owned value only at the moment it actually needs to modify it.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll use std::borrow::Cow to write functions that avoid needless allocation, return Cow<str>, convert with into_owned, and measure exactly when an allocation happens.
What You'll Learn in This Lesson
- What Clone-on-Write means in practice
- The Borrowed and Owned variants of Cow
- Returning Cow<str> to skip allocation
- Allocating only when data must change
- Turning a Cow into a String with into_owned
- Counting exactly when allocations happen
1️⃣ Borrowed or Owned, On Demand
Cow<str> is an enum with two variants: Cow::Borrowed(&str) wraps a reference with no allocation, and Cow::Owned(String) wraps a freshly allocated value. A function returns Borrowed on the common "nothing changed" path and Owned only when it must produce new data.
use std::borrow::Cow;
// Cow holds EITHER a borrowed reference OR an owned value.
// Here we only allocate a new String when there's something to trim.
fn trimmed(input: &str) -> Cow<'_, str> {
if input.trim().len() == input.len() {
// Nothing to change — borrow the original, no allocation.
Cow::Borrowed(input)
} else {
// We must change it — allocate a new owned String.
Cow::Owned(input.trim().to_string())
}
}
fn main() {
let clean = trimmed("hello"); // already trimmed -> Borrowed
let dirty = trimmed(" spaced "); // needs trimming -> Owned
println!("clean = '{}'", clean);
println!("dirty = '{}'", dirty);
// matches!(...) lets us see which variant we got.
println!("clean is borrowed? {}", matches!(clean, Cow::Borrowed(_)));
println!("dirty is owned? {}", matches!(dirty, Cow::Owned(_)));
}Because Cow derefs to str, you can print it and call string methods on it directly — callers usually don't even need to know which variant they got.
2️⃣ Allocating Only When You Must
The payoff is in functions that transform text. If the input already meets the requirement, return it borrowed — zero cost. If it needs changing, do the work and return it owned. When you finally need a plain owned value, call into_owned(), which clones only if the Cow was still borrowed.
use std::borrow::Cow;
// Replace spaces with underscores — but only allocate if a space exists.
fn slugify(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_")) // changed -> owned
} else {
Cow::Borrowed(input) // unchanged -> borrowed
}
}
fn main() {
let a = slugify("already_clean");
let b = slugify("needs cleaning up");
// into_owned() always gives you a real, owned String,
// cloning only if the Cow was still borrowed.
let owned_a: String = a.into_owned();
let owned_b: String = b.into_owned();
println!("a -> {}", owned_a);
println!("b -> {}", owned_b);
}Here "already_clean" needed no change, so its into_owned() performed a single clone, while "needs cleaning up" was already owned from the replace, so into_owned() was free.
3️⃣ Proving the Allocation Savings
To make the benefit concrete, we can count how many times the Owned variant appears across a batch of inputs. Every unchanged message stays Borrowed and costs nothing; only the messages that actually needed editing allocate.
use std::borrow::Cow;
// Censor a banned word, but avoid allocating for clean messages.
fn censor(message: &str) -> Cow<'_, str> {
if message.contains("secret") {
Cow::Owned(message.replace("secret", "*****"))
} else {
Cow::Borrowed(message)
}
}
fn main() {
let messages = ["hello world", "my secret plan", "all clear"];
let mut allocations = 0;
for &m in messages.iter() {
let result = censor(m);
if let Cow::Owned(_) = result {
allocations += 1; // we only count when we actually allocated
}
println!("{}", result);
}
println!("allocations made: {}", allocations);
}Out of three messages, only one contained the banned word, so exactly one allocation happened. With a plain String return type, all three would have allocated — Cow saved two-thirds of the work.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — return Borrowed when calm, Owned when loud.
use std::borrow::Cow;
fn maybe_shout(input: &str, loud: bool) -> Cow<'_, str> {
if loud {
// Uppercasing CHANGES the text, so we must own it.
Cow::___(input.to_uppercase()) // 👉 the owning variant
} else {
// No change needed — just borrow.
Cow::___(input) // 👉 the borrowing variant
}
}
fn main() {
println!("{}", maybe_shout("calm", false));
println!("{}", maybe_shout("loud", true));
}
// ✅ Expected output:
// calm
// LOUDMini-Challenge: Ensure a URL Prefix
Write a function that prepends a prefix only when it is missing, borrowing the input otherwise. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: ensure a URL prefix without needless allocation.
//
// Write fn ensure_prefix<'a>(input: &'a str, prefix: &str) -> Cow<'a, str>
// that:
// - returns Cow::Borrowed(input) when input already starts with prefix
// (no allocation needed), and
// - returns Cow::Owned(format!("{}{}", prefix, input)) otherwise.
//
// In main, call it on "https://site.com" (already prefixed) and on
// "site.com" (needs the prefix), printing each result.
//
// ✅ Expected output:
// https://site.com
// https://site.com
use std::borrow::Cow;
fn main() {
// your code here
}Common Errors (and the fix)
- ❌ Forgetting to import Cow. ✅ Add use std::borrow::Cow; at the top of the file.
- ❌ Putting a String in Cow::Borrowed. ✅ Borrowed takes a reference (&str); the owned variant takes the String.
- ❌ Returning a Cow that outlives its borrow. ✅ The returned lifetime is tied to the borrowed input; annotate with <'a> when needed.
- ❌ Calling to_string() everywhere anyway. ✅ That defeats the purpose — only allocate on the branch that truly modifies the data.
Pro Tips
- 💡 Cow shines on the no-op path. Reach for it when most inputs pass through unchanged.
- 💡 to_mut() gives a &mut to the data, cloning to owned first if needed — handy for in-place edits.
- 💡 It's not just for strings. Cow<[T]> works for slices too, with the same borrow-or-own logic.
📋 Quick Reference — Cow
| Item | Meaning |
|---|---|
| Cow::Borrowed(x) | Wrap a reference, no allocation |
| Cow::Owned(x) | Wrap a freshly allocated value |
| .into_owned() | Get an owned value (clone if borrowed) |
| .to_mut() | Mutable ref, cloning to owned first |
| Cow<'a, str> | Borrowed-or-owned string |
🎉 Lesson Complete!
- ✅ Cow = Clone on Write: Borrowed for free, Owned when you must change data
- ✅ Returning Cow<str> avoids allocating on the unchanged path
- ✅ into_owned() yields a String, cloning only if it was borrowed
- ✅ You allocate exactly when — and only when — you actually modify the value
Practice quiz
What does Cow stand for?
- Copy on Write
- Container of Wrappers
- Clone on Write
- Closure over Writes
Answer: Clone on Write. Cow is Clone on Write: it borrows for free and only allocates a clone at the moment it must modify the data.
Which two variants does the Cow enum have?
- Borrowed and Owned
- Some and None
- Ok and Err
- Read and Write
Answer: Borrowed and Owned. Cow::Borrowed holds a reference (no allocation); Cow::Owned holds a freshly allocated value.
Which module is Cow imported from?
- std::rc
- std::cell
- std::clone
- std::borrow
Answer: std::borrow. You write use std::borrow::Cow; to bring it into scope.
On the common path where data is unchanged, which variant should a function return?
- Cow::Owned
- Cow::Borrowed
- Cow::Clone
- Cow::None
Answer: Cow::Borrowed. Return Cow::Borrowed when nothing changed so you pay no allocation cost.
What does into_owned() do?
- Always gives a fully owned value, cloning only if it was borrowed
- Always borrows the data
- Frees the value
- Converts Owned back to Borrowed
Answer: Always gives a fully owned value, cloning only if it was borrowed. into_owned() yields an owned value like String; it clones only when the Cow was still Borrowed.
What does Cow::Borrowed take as its argument?
- An owned String
- A Vec
- A reference such as &str
- A closure
Answer: A reference such as &str. Borrowed wraps a reference (like &str); the Owned variant takes the owned String.
Why return Cow<str> instead of always returning String?
- It is faster to type
- It avoids allocating on the unchanged path
- It prevents borrowing
- Strings can't be returned
Answer: It avoids allocating on the unchanged path. Always returning String allocates even when nothing changed; Cow borrows for free on the no-change path.
In a censor function over 3 messages where only 1 contains the banned word, how many allocations (Owned variants) happen?
- 0
- 2
- 3
- 1
Answer: 1. Only the message that actually needed editing allocates, so exactly one Owned variant is produced.
What does Cow's to_mut() do?
- Returns an immutable reference
- Gives a &mut to the data, cloning to owned first if needed
- Deletes the value
- Converts to a slice
Answer: Gives a &mut to the data, cloning to owned first if needed. to_mut() returns a mutable reference, cloning the borrowed data into an owned value first if necessary.
Roughly how much runtime overhead does Cow add on the common unchanged path?
- A full heap allocation every time
- It always doubles memory use
- Very little — it avoids allocation on the common path
- It requires a background thread
Answer: Very little — it avoids allocation on the common path. Cow is a small enum; on the unchanged path it skips heap allocation entirely, so it is typically a win.
Continue this course
- Previous: The Newtype Pattern & Type Aliases
- Next: Error Handling with Box<dyn Error> — Mix error types with ? and a boxed trait object, including in main()
- Quick reference: Rust cheat sheet
Frequently asked questions
What does Cow stand for and what is it for?
Cow stands for Clone on Write. It is an enum in std::borrow with two variants: Borrowed, which holds a reference to existing data, and Owned, which holds a freshly allocated value. The point of Cow is to defer allocation: you work with borrowed data for free, and only pay the cost of cloning or allocating a new owned value at the exact moment you actually need to modify it. It is ideal for functions that usually return their input unchanged but occasionally need to produce a modified copy.
When should I return Cow<str> instead of String or &str?
Return Cow<str> when a function sometimes returns its input unchanged and sometimes returns a modified version. If you always returned String, you would allocate even when nothing changed, wasting work. If you returned &str, you could not return a modified version at all, because the new data would have no owner. Cow gives you the best of both: the common no-change path borrows for free, and only the rare modifying path allocates. Trimming, escaping, censoring, and normalizing strings are classic use cases.
What does into_owned do?
into_owned consumes the Cow and always gives you a fully owned value, such as a String. If the Cow was already the Owned variant, it just hands back the value it was holding with no extra work. If the Cow was Borrowed, into_owned clones the borrowed data into a new owned value. This is how you turn a Cow into a plain owned type when you need to store it somewhere with no lifetime constraints. The clone only happens in the borrowed case, so you pay for it only when necessary.
Does Cow add runtime overhead?
Very little. A Cow is just an enum with a one-byte-ish tag plus the borrowed reference or owned value, so checking which variant you have is cheap. The whole design exists to AVOID the bigger cost — heap allocation and copying — on the common path where data is unchanged. So in the typical case Cow is faster than unconditionally allocating, and in the worst case it is about the same as allocating directly. Used where it fits, it is a clear performance win and a clean API at the same time.