Mastering Rust’s Cow for Zero‑Alloc String Handling
What is Cow?
When I needed to process user‑provided text without forcing a copy, I reached for Cow<str>. Cow (short for Copy on Write
) is an enum that represents either an owned String or a borrowed &str. The variant is chosen at runtime based on whether you need to modify the data.
The definition is simple:
pub enum Cow<'a, T> {
Borrowed(&'a T),
Owned(T),
}
Because the owned variant is a T and the borrowed variant is a reference, you can pass a Cow<str> wherever a &str is expected, and you can mutate it only when you own it. This dual nature makes Cow a zero‑cost abstraction for situations where you are unsure whether you will need to modify the data.
Why Choose Cow over String?
Consider a function that normalizes a filename for logging:
fn log_filename(path: &str) {
let normalized = if path.starts_with('./') {
// Remove leading './' if present
&path[2..]
} else {
path
};
println!("Processing: {}", normalized);
}
With a plain String, you would have to allocate a new string whenever you need to strip the prefix, even when the input already lacks it. Using Cow lets you avoid that allocation:
fn log_filename_cow(path: Cow<str>) {
let normalized = if path.starts_with('.') && path.starts_with('./') {
// We own the data now, so we can safely mutate it
let mut owned = path.into_owned();
owned.drain(..2);
owned
} else {
// No mutation needed, keep the borrowed slice
path
};
println!("Processing: {}", normalized);
}
If the input already does not contain "./", the function never allocates, and the borrow is returned as‑is. Only when modification is required does the code take ownership and perform the mutation. This pattern is especially valuable in high‑throughput services where allocating many temporary strings can become a bottleneck.
Real‑World Example: Building Log Messages
In a distributed system I worked on, we collected error details from multiple sources – database queries, network responses, and user input. The logging routine accepted a slice of Cow<str> so that each component could contribute without forcing an allocation unless it needed to format the message.
Using Cow lets you keep the API ergonomic while still giving you the freedom to mutate when necessary.
Here is a simplified version of the logger:
use std::fmt::Display;
fn format_log parts: &[Cow<str>] -> String {
let mut output = String::new();
for (i, part) in parts.iter().enumerate() {
if i > 0 {
output.push_str(' | ');
}
// No allocation if we only need to reference the part
output.push_str(part);
}
output
}
Each part can be a borrowed slice from a larger buffer or an owned string that was previously mutated. The function works uniformly because Cow<str> implements Display and can be coerced to &str via as_ref(). The only time an allocation occurs is when the caller actually owns the data and needs to modify it before passing it in.
Integrating Cow with Serde
When serializing data to JSON, many crates accept String or &str. If you already have a Cow<str>, you can let serde handle the conversion automatically because Cow<T> implements Serialize when T does. This is handy for APIs that may want to avoid allocating a new string for every response.
use serde::Serialize;
#[derive(Serialize)]
struct ApiResponse {
message: Cow<str>,
}
fn main() {
let borrowed = Cow::Borrowed(&'Hello, world!');
let owned = Cow::Owned(String::from('Hello, world!'));
let resp1 = ApiResponse { message: borrowed };
let resp2 = ApiResponse { message: owned };
// Both can be serialized without extra copies
println!("{}", serde_json::to_string(&resp1).unwrap());
}
Notice that the owned variant will be serialized as a String while the borrowed variant is serialized as a string slice. The serializer does not need to allocate a new String for the borrowed case, which can be a subtle performance win when dealing with many small messages.
Pitfalls and When to Avoid Cow
Cow is not a silver bullet. If you plan to mutate the data frequently, the copy on write
behavior can lead to unexpected allocations because each mutation will trigger a copy. In such cases a plain String is simpler and more predictable.
Another consideration is lifetimes. Using Cow<str> ties the data to the scope of the reference you pass in. If you need to store the value beyond that scope, you must convert it to an owned String explicitly:
let mut cow: Cow<str> = Cow::Borrowed(&'temporary');
// Later, when we need a long‑lived value
cow = Cow::Owned(cow.into_owned());
Finally, remember that pattern matching on Cow is straightforward but can be verbose. The as_ref() method or the fn as_ref(&self) -> &T implementation often eliminates the need for explicit matches.
Wrapping Up
Mastering Cow gives you a versatile tool for writing zero‑allocation code without sacrificing ergonomics. By understanding when you need ownership versus when a borrowed slice suffices, you can reduce unnecessary heap operations and keep your APIs clean.
Try integrating Cow into a routine that currently allocates strings repeatedly. You may be surprised at how much simpler and faster the code becomes.