Step 5 of 6
Cow: borrow when you can, own when you must
Some functions usually return their input unchanged but occasionally need to modify it: escaping text that rarely contains special characters, normalizing paths that are usually already normal. Returning String allocates every time, even for the common case. Returning &str can't express the modified case.
Cow ("clone on write", from std::borrow) handles both. Cow<'a, str> is an enum with two variants: Borrowed(&'a str) or Owned(String).
use std::borrow::Cow;
fn escape_html(input: &str) -> Cow<'_, str> {
if !input.contains(['<', '>', '&']) {
return Cow::Borrowed(input); // the common case: no allocation
}
let mut out = String::with_capacity(input.len() + 16);
for c in input.chars() {
match c {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
_ => out.push(c),
}
}
Cow::Owned(out)
}
fn main() {
for text in ["hello crab", "1 < 2 & 3 > 2"] {
let escaped = escape_html(text);
let kind = match &escaped {
Cow::Borrowed(_) => "borrowed",
Cow::Owned(_) => "owned",
};
println!("{kind}: {escaped}");
}
}
borrowed: hello crab
owned: 1 < 2 & 3 > 2
Cow<'_, str>borrows frominput(the'_is the elided lifetime ofinput).- Callers don't care which variant they got:
Cow<str>dereferences to&str, so it prints, compares and slices like one..into_owned()gives aString(allocating only if it was borrowed). .to_mut()turns aBorrowedinto anOwnedcopy the first time you need to change it, and returns&mutto the owned value. That's the "clone on write" in the name.
std uses Cow too: String::from_utf8_lossy(bytes) returns Cow::Borrowed when the bytes were valid UTF-8 and Cow::Owned only when it had to replace bad bytes. A Cow<'static, str> field is a common way to hold messages that are usually string literals but sometimes built with format!.
When not to use it
Cow adds a lifetime and a little complexity to every caller. It pays off when the unchanged case is common and the data is big or the function is hot. If you modify the input most of the time anyway, just return String.
Common mistakes
Cow::Owned(s)with a&str: the owned variant holds aString, so writeCow::Owned(s.to_string()).- Calling
.to_string()on the result "to be safe": that allocates every time and throws the benefit away. Use theCowas a&strwhere you can. - Returning
Cow::Borrowedof a localString: the borrow can't outlive the function. Anything you built inside must beOwned.
Your task
Write redact_digits, which replaces every ASCII digit with # ("pin 1234" becomes "pin ####"). Text without digits must come back as Cow::Borrowed (no allocation); a hidden test checks the variant.
Challenge 2
Write ensure_trailing_slash, which makes sure a directory path ends with /: "/usr/bin" becomes "/usr/bin/" (owned), "/usr/bin/" comes back unchanged (borrowed), and "" becomes "/".
Challenge 3
Cow works for slices too: Cow<'_, [i32]> is either a borrowed &[i32] or an owned Vec<i32>. Write clamp_negatives, which replaces negative numbers with 0. Start with Cow::Borrowed(values) and call .to_mut() only when you find a negative number: the first call copies the slice into a Vec, later calls reuse it. A slice with no negatives comes back borrowed.
Previous: Iterator chains and avoiding intermediate collections Next: Benchmarks and profilers