We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 17 of 21
Cow: borrow until you have to own
Replace every ASCII control character in a string with '_' — and do not
allocate when there is nothing to replace.
use std::borrow::Cow;
pub fn sanitize(s: &str) -> Cow<'_, str>
The harness reports two things per case: the resulting text, and whether you
returned Cow::Borrowed or Cow::Owned. So “it did not allocate” is not a
hope you have about your code — it is an asserted value in the test output.
"clean text" -> text "clean text", borrowed true
"a\tb" -> text "a_b", borrowed false
"" -> text "", borrowed true
char::is_ascii_control defines the character class: 0x00–0x1F and
0x7F. Tab, newline and carriage return are control characters. Non-ASCII
letters are not.
The problem Cow solves
You have a function that usually returns its input unchanged and occasionally has to build something new. Sanitising, normalising, unescaping, path canonicalisation, trimming — the shape is everywhere. Two obvious signatures, both bad:
fn sanitize(s: &str) -> String // always allocates, even for clean input
fn sanitize(s: &str) -> &str // impossible: what do you return when you changed it?
The second one is not a style preference, it is a hard wall. A &str must
point at memory that outlives the call, and a freshly built string is owned by
the function. You met that as E0515 in the zero-copy parsing problem.
Cow — “clone on write” — is the type-level answer:
pub enum Cow<'a, B: ?Sized + ToOwned> {
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
For B = str, that is exactly Borrowed(&'a str) or Owned(String). One
discriminant, one branch. And crucially, the choice happens at runtime,
per call — the caller gets zero copies on the common path and a correct
answer on the uncommon one, from a single signature.
Using one
Cow<'a, B> implements Deref<Target = B>, so every read-only method of the
borrowed type works without you caring which variant you hold:
let c = sanitize(input);
c.len(); c.contains("x"); c.starts_with('/'); // all fine
Three methods matter:
-
into_owned()consumes theCowand gives you aString— a move if it wasOwned, a clone if it wasBorrowed. This is what you call when you are done deferring. -
to_mut()gives you a&mut String, promoting aBorrowedtoOwnedby cloning first if necessary. This is the “write” in copy-on-write, and it is how you mutate aCowin place. -
matches!(c, Cow::Borrowed(_))— pattern matching, which is what the harness uses to grade you.
The trap in the starter
The starter builds the output unconditionally:
let cleaned: String = s.chars().map(...).collect();
Cow::Owned(cleaned)
This is correct and it defeats the entire point. collect::<String>()
allocates. So does str::replace — which is the other one-liner people
reach for, and worth knowing about explicitly: replace always returns a
new String, even when it replaced nothing. There is no fast path inside
it.
The fix is a cheap check first: scan for a control character, and only build a new string if you find one. Yes, that is two passes over the input in the bad case. Two passes over cache-hot bytes is enormously cheaper than an allocation, and in the good case — which is the common case — it is one pass and no allocation at all.
::: question Two passes in the bad case feels wasteful. Could you do it in one? You can: find the index of the first control character, and if there is one, build the output starting from the already-clean prefix.
match s.find(|c: char| c.is_ascii_control()) {
None => Cow::Borrowed(s),
Some(i) => {
let mut out = String::with_capacity(s.len());
out.push_str(&s[..i]);
out.extend(s[i..].chars().map(|c| if c.is_ascii_control() { '_' } else { c }));
Cow::Owned(out)
}
}
This is what a serious library does — it is roughly how serde_json escapes
strings and how percent-encoding works. It is also four times as long and
has an off-by-one you can get wrong.
For a function called once per request, write the two-pass version. For one called once per byte of a gigabyte, write this. Knowing both, and knowing which situation you are in, is the actual skill. :::
Two clippy lints that exist because Cow is easy to get wrong
suspicious_to_owned is default-on, and it catches a genuinely dangerous
silent bug. Verified message from clippy 0.1.95:
error: this `to_owned` call clones the `Cow<'_, str>` itself and does not
cause its contents to become owned
cow.to_owned() type-checks and does nothing you wanted: Cow implements
Clone, so to_owned clones the wrapper, still Borrowed, still tied to
the same lifetime. You almost always meant into_owned(). Without the lint
this compiles and then fails later with a confusing lifetime error, or
quietly keeps a borrow alive.
owned_cow is also default-on, and catches the number-one Cow mistake:
writing the owned type as the inner type.
struct Message { text: Cow<'static, String> } // error: needlessly owned Cow type
Cow<'a, B> wants B to be the borrowed form — str, not String;
[T], not Vec<T>; Path, not PathBuf. Write Cow<'static, String> and
the Borrowed variant holds a &String, so the type is now
“either a reference to a heap string or a heap string” — a pointless
indirection that can never avoid an allocation, which was the entire purpose.
When not to use Cow
Be as clear about this as about the rest. If your function always modifies
its input, Cow costs you a discriminant, a branch at every use site, and a
more complicated signature, in exchange for nothing. Just return String.
And if you find yourself adding a Cow field to a struct, read the next
problem in this track first — it makes the whole struct generic over a
lifetime, and that cascades into every signature that touches it.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.