Cow in Rust
Sometimes we need a function that returns a string. Most of the time, the string is already in the form we want, so we could simply return a reference to it. But occasionally, we need to modify it and return a new string.
We could always return a String, but that means allocating memory even when nothing needs to change.
Rust has a type for exactly this situation: Cow.
Cow stands for Clone on Write. It lets us borrow data when possible and create an owned copy only when necessary.
Let’s see how it works.
The Problem
Imagine we’re writing a function that ensures a URL path starts with /.
If the path already starts with /, we can return it unchanged. Otherwise, we need to add the missing slash.
Here’s a straightforward implementation:
fn with_leading_slash(path: &str) -> String {
if path.starts_with('/') {
path.to_string()
} else {
format!("/{path}")
}
}This works, but there’s a problem.
Consider:
let path = with_leading_slash("/products");The path already starts with /. We don’t need to change anything.
Yet we’re calling to_string(), which creates a new String, allocates memory on the heap, and copies the original contents into it.
That’s unnecessary work.
Could we return &str instead?
fn with_leading_slash(path: &str) -> &str {
if path.starts_with('/') {
path
} else {
// What do we return here?
}
}The first branch is easy. We can return the original reference.
But the second branch needs to construct a new string. We can’t return a reference to a locally created String because that string would be dropped when the function returns.
What we really want is a return type that can represent either borrowed or owned data.
That’s what Cow gives us.
Cow
Cow lives in std::borrow:
use std::borrow::Cow;Let’s rewrite our function:
use std::borrow::Cow;
fn with_leading_slash(path: &str) -> Cow<'_, str> {
if path.starts_with('/') {
Cow::Borrowed(path)
} else {
Cow::Owned(format!("/{path}"))
}
}Now we have two possibilities.
When the path already starts with /, we return Cow::Borrowed(path). This simply stores a reference to the original string. No new string allocation is needed.
Otherwise, we return Cow::Owned(...), which stores the newly constructed String.
Let’s try both:
fn main() {
let a = with_leading_slash("/products");
let b = with_leading_slash("products");
println!("{a}");
println!("{b}");
}Both print /products.
But internally, they take different paths:
aborrows the original string without allocating.bowns a newly allocatedString.
And the caller doesn’t need to care which variant it received just to read the string.
That’s the useful part of Cow. We can expose one return type while deciding whether ownership is necessary.
How Does Cow Work?
Under the hood, Cow is an enum.
Its definition in the standard library looks roughly like this:
pub enum Cow<'a, B>
where
B: ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}There are two variants:
Borrowed holds a reference to existing data.
Owned holds an owned version of that data.
The ToOwned trait determines what the owned representation should be.
For example:
| Cow type | Borrowed | Owned |
|---|---|---|
Cow<'a, str> | &'a str | String |
Cow<'a, [T]> | &'a [T] | Vec<T> |
Cow<'a, Path> | &'a Path | PathBuf |
So Cow isn’t limited to strings. It works with slices, paths, and other types that support ToOwned.
The lifetime 'a describes how long borrowed data must remain valid. If the Cow owns its data, it doesn’t depend on that borrowed value remaining alive.
There’s another useful detail: Cow implements Deref.
That means we can call read-only methods on the underlying data directly:
let text: Cow<'_, str> = Cow::Borrowed("hello");
println!("{}", text.len());
println!("{}", text.starts_with('h'));We don’t have to manually check whether the value is borrowed or owned.
Both variants provide the same read-only interface.
What Happens When We Modify It?
So far, we’ve only used Cow to return borrowed or owned values.
But the name Clone on Write suggests something else: cloning the data when we try to modify it.
That’s where to_mut() comes in.
Consider:
use std::borrow::Cow;
fn main() {
let original = String::from("hello");
let mut text: Cow<'_, str> = Cow::Borrowed(&original);
text.to_mut().push_str(" world");
println!("{text}");
println!("{original}");
}This prints:
hello world
helloNotice that original hasn’t changed.
Initially, text borrows the original String. It doesn’t own the contents.
When we call:
text.to_mut()Rust checks which variant the Cow currently holds.
Because it’s borrowed, Rust creates an owned copy of the string and changes the Cow to its Owned variant.
Only then does push_str() modify the string.
The original data remains untouched.
Now consider what happens if the Cow already owns its data:
let mut text: Cow<'_, str> =
Cow::Owned(String::from("hello"));
text.to_mut().push_str(" world");This time, to_mut() doesn’t need to clone anything.
The Cow already owns a String, so it simply returns a mutable reference to that string.
That’s the basic rule:
- If the value is borrowed,
to_mut()creates an owned copy. - If the value is already owned,
to_mut()gives us mutable access to the existing allocation.
And once a borrowed Cow becomes owned, subsequent calls to to_mut() don’t need to clone it again.
This is why the cloning is described as lazy. The data is only copied when we actually request mutable access.
Converting Cow Into Owned Data
Sometimes we eventually need full ownership, regardless of whether the Cow currently borrows its data.
For that, we have into_owned():
let text: Cow<'_, str> = Cow::Borrowed("hello");
let owned: String = text.into_owned();Because text is borrowed, Rust creates a new String.
But if we already have an owned value:
let text: Cow<'_, str> =
Cow::Owned(String::from("hello"));
let owned: String = text.into_owned();Rust simply moves the existing String out of the Cow.
No additional string clone is required.
A Real Example From the Standard Library
One of my favorite examples of Cow is String::from_utf8_lossy().
This function takes a byte slice and converts it into a string, replacing invalid UTF-8 sequences with the replacement character.
Its signature is:
pub fn from_utf8_lossy(v: &[u8]) -> Cow<'_, str>Why doesn’t it simply return a String?
Because a byte slice might already contain valid UTF-8.
In that case, Rust can return a borrowed string slice without allocating a new string.
But when the bytes contain invalid UTF-8, the function needs to construct a new string with replacement characters.
Let’s see:
use std::borrow::Cow;
fn main() {
let valid = String::from_utf8_lossy(b"hello");
let invalid = String::from_utf8_lossy(b"hello\xFF");
println!("{}", matches!(valid, Cow::Borrowed(_)));
println!("{}", matches!(invalid, Cow::Owned(_)));
}This prints:
true
trueThe first call borrows the original bytes as valid UTF-8.
The second call creates an owned string because it needs to replace the invalid byte.
This is a perfect use case for Cow: the common path can avoid allocating, while the exceptional path still returns the corrected data.
When Should You Use Cow?
Cow is useful when an operation can often reuse existing data but occasionally needs to produce a modified version.
Some examples include:
- String normalization where most inputs are already valid.
- Parsing or decoding functions that only allocate when transformations are necessary.
- Configuration values that normally borrow defaults but sometimes need modification.
- Working with borrowed slices that may need to be converted into owned vectors.
But that doesn’t mean we should use it everywhere.
If a function only needs to read a string, &str is usually sufficient.
If the function always needs ownership, returning String is often simpler.
And if every call is going to modify the data anyway, Cow may not provide any allocation savings.
There’s also a small cost to handling the enum and checking which variant is present, although the compiler can often optimize that away.
Wrapping Up
Cow provides a convenient way to work with data that might be borrowed or owned.
Instead of allocating every time, we can borrow existing data when no changes are necessary and create an owned copy only when we need one.
The important things to remember are:
Cow::Borrowedreferences existing data without cloning it.Cow::Ownedstores an owned value.to_mut()clones borrowed data before allowing mutation, but reuses already owned data.into_owned()gives us an owned value, cloning only when necessary.
It’s a relatively small abstraction, but it captures a useful idea: don’t copy data until there’s a reason to.