Cow<str> is the type everyone reaches for when a function might need to modify its input. Cow::Borrowed and Cow::Owned are the constructors that get the spotlight; to_mut is the third piece, and it’s the one that actually pays off the laziness.
What to_mut does
to_mut takes &mut Cow<str> and hands back &mut String:
- If the
Cow is already Owned, you get a direct &mut to the inner String. - If it’s
Borrowed, to_mut clones the slice into a fresh String, swaps the Cow over to Owned, and then hands you the mutable reference.
That asymmetry is the whole point. Many callers borrow and never touch to_mut — they never allocate. The ones that do call it pay the allocation cost exactly once, on first write.
A walking-the-string example
Expand \t into two spaces, but only allocate if the input actually contains a tab:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| use std::borrow::Cow;
fn expand_tabs(s: &str) -> Cow<'_, str> {
let mut out: Cow<'_, str> = Cow::Borrowed(s);
if let Some(i) = s.find('\t') {
// First write — `to_mut` clones the slice into a String, then we
// rebuild from byte `i` onwards.
let buf = out.to_mut();
buf.truncate(i);
for c in s[i..].chars() {
if c == '\t' {
buf.push_str(" ");
} else {
buf.push(c);
}
}
}
out
}
|
The happy path — input has no tab — never enters the if, never allocates, and returns the original slice wrapped in Cow::Borrowed. The unhappy path allocates exactly once.
to_mut really earns its keep when you chain several optional mutations. The first one that fires flips the Cow to Owned; every following mutation sees an already-owned buffer and reuses it:
1
2
3
4
5
6
7
8
9
10
11
12
| use std::borrow::Cow;
fn apply_rules<'a>(s: &'a str, rules: &[(char, &str)]) -> Cow<'a, str> {
let mut out: Cow<'a, str> = Cow::Borrowed(s);
for &(from, to) in rules {
if out.contains(from) {
let replaced = out.replace(from, to);
*out.to_mut() = replaced;
}
}
out
}
|
Three things worth pointing at. First, out.contains(from) works because Cow<str> derefs to str. Second, the assignment *out.to_mut() = replaced replaces the inner String, not the Cow itself. Third, once the first rule fires, all subsequent to_mut calls are a no-op &mut String — no extra clones.
Pitfall: to_mut always commits
There’s no “preview, then maybe commit” mode. Calling to_mut on a borrowed Cow clones immediately, even if you never end up writing through the returned reference. So this is a trap:
1
2
3
4
| if !out.is_empty() {
let _ = out.to_mut(); // allocates even though we may not change anything
// ... maybe mutate, maybe not
}
|
Guard the call with the actual condition that means “I’m about to write,” not the condition that means “I might.” The mental shortcut: to_mut is the moment you trade your &str for a String. Reach for it lazily, but commit completely.