484: Cow<str> for Flexible Strings
Difficulty: 2 Level: Intermediate Avoid unnecessary heap allocations by holding either a borrowed `&str` or an owned `String` โ decided at runtime.The Problem This Solves
Imagine a function that sanitizes a string: if it's already clean, return it as-is; if it needs modification, return the modified version. In Python or JavaScript, you'd return a new string either way โ even if nothing changed, you allocate. In Rust, returning a `String` means you always allocate a copy. Returning `&str` means you can never allocate. Neither is right. `Cow<'a, str>` ("Clone on Write") solves this. It's an enum that holds either a borrowed `&str` or an owned `String`. If your data needs no modification, you return `Cow::Borrowed(s)` โ zero allocation, the original `&str` passed through. If modification is needed, you return `Cow::Owned(new_string)` โ allocated only when necessary. This pattern appears throughout the standard library. `String::from_utf8_lossy()` returns `Cow<str>` โ if the input is valid UTF-8, you get a borrowed view; if replacement was needed, you get an owned `String`. Many serialization and normalization functions use the same approach.The Intuition
`Cow<str>` is Rust's way of saying "I might need to own this, I might not โ decide at runtime." The mental model:- `Cow::Borrowed(&str)` โ "I'm just looking, not touching."
- `Cow::Owned(String)` โ "I made a copy and modified it."
How It Works in Rust
use std::borrow::Cow;
// Returns borrowed if no spaces, owned if spaces found
fn ensure_no_spaces(s: &str) -> Cow<str> {
if !s.contains(' ') {
Cow::Borrowed(s) // zero allocation โ just a view
} else {
Cow::Owned(s.replace(' ', "_")) // allocates only when needed
}
}
let clean = ensure_no_spaces("hello_world");
let dirty = ensure_no_spaces("hello world");
// Both work the same โ Cow deref's to &str transparently
println!("{}", clean); // "hello_world" (no allocation)
println!("{}", dirty); // "hello_world" (allocated)
// Check which variant at runtime
matches!(clean, Cow::Borrowed(_)) // true
matches!(dirty, Cow::Owned(_)) // true
// Accept both &str and String in one parameter type
fn process(input: Cow<str>) -> String {
format!("processed: {}", input.trim()) // works on both variants
}
process(Cow::Borrowed(" hello "));
process(Cow::Owned(String::from(" world ")));
// Cow::into_owned() โ get a String regardless of variant
let s: String = clean.into_owned(); // copies if Borrowed, moves if Owned
// from_utf8_lossy returns Cow<str>
let bytes = b"hello world";
let cow = String::from_utf8_lossy(bytes); // Borrowed (valid UTF-8, no copy)
What This Unlocks
- Zero-cost sanitization functions โ return borrowed input unchanged, allocate only on modification.
- Flexible API design โ functions that accept `Cow<str>` work with both `&str` literals and owned `String` values.
- Efficient lossy UTF-8 conversion โ `from_utf8_lossy()` avoids allocation for valid input.
Key Differences
| Concept | OCaml | Rust | |
|---|---|---|---|
| Borrow or own | No distinction (strings immutable) | `Cow<str>` โ explicit borrowed vs owned | |
| Conditional allocation | Always allocates on transform | `Cow::Borrowed` avoids allocation | |
| Uniform API | N/A | `Deref<Target=str>` โ same methods on both | |
| Runtime check | N/A | `matches!(cow, Cow::Borrowed(_))` | |
| Force owned | N/A | `.into_owned()` โ `String` | |
| Closest analog | `type cow_str = Borrowed of string \ | Owned of Buffer.t` | `Cow<'a, str>` โ built into std |
// 484. Cow<str> for flexible strings
use std::borrow::Cow;
fn ensure_no_spaces(s: &str) -> Cow<str> {
if !s.contains(' ') {
Cow::Borrowed(s) // no allocation!
} else {
Cow::Owned(s.replace(' ', "_")) // allocates only when needed
}
}
fn to_uppercase_if_needed(s: Cow<str>) -> Cow<str> {
if s.chars().any(|c| c.is_lowercase()) {
Cow::Owned(s.to_uppercase())
} else {
s // pass through unchanged
}
}
fn process(input: Cow<str>) -> String {
// Can call String methods on Cow<str> via deref
format!("processed: {}", input.trim())
}
fn main() {
let clean = ensure_no_spaces("hello_world");
let dirty = ensure_no_spaces("hello world");
println!("clean borrowed: {}", matches!(clean, Cow::Borrowed(_)));
println!("dirty owned: {}", matches!(dirty, Cow::Owned(_)));
println!("{}", clean);
println!("{}", dirty);
// Accept both &str and String
let s1: Cow<str> = Cow::Borrowed("HELLO");
let s2: Cow<str> = Cow::Owned(String::from("hello"));
println!("{}", to_uppercase_if_needed(s1));
println!("{}", to_uppercase_if_needed(s2));
println!("{}", process(Cow::Borrowed(" hi ")));
println!("{}", process(Cow::Owned(String::from(" owned "))));
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn test_no_alloc() { assert!(matches!(ensure_no_spaces("nospace"), Cow::Borrowed(_))); }
#[test] fn test_allocs() { assert!(matches!(ensure_no_spaces("has space"), Cow::Owned(_))); }
#[test] fn test_content() { assert_eq!(&*ensure_no_spaces("a b"), "a_b"); }
}
(* 484. Cow<str> concept โ OCaml *)
(* OCaml has no Cow; simulate with a type *)
type cow_str = Borrowed of string | Owned of Buffer.t
let make_borrowed s = Borrowed s
let make_owned s = Owned (let b=Buffer.create (String.length s) in Buffer.add_string b s; b)
let to_string = function
| Borrowed s -> s
| Owned b -> Buffer.contents b
let ensure_uppercase = function
| Borrowed s ->
if String.for_all (fun c -> not(c>='a'&&c<='z')) s then Borrowed s
else Owned (let b=Buffer.create (String.length s) in
String.iter (fun c -> Buffer.add_char b (Char.uppercase_ascii c)) s; b)
| Owned b as o -> o
let () =
let a = make_borrowed "HELLO" in
let b = make_borrowed "hello" in
let ra = ensure_uppercase a in
let rb = ensure_uppercase b in
Printf.printf "%s %s\n" (to_string ra) (to_string rb);
(* a was not re-allocated *)
Printf.printf "a is borrowed: %b\n" (match ra with Borrowed _ -> true | _ -> false)