July 30, 2025•6 min

How does Cow<'a, B> (Copy-on-Write) work in Rust?

m
mayo

Cow<'a, B> (Copy-on-Write) is a smart pointer in Rust’s std::borrow module that provides a clone-free abstraction over borrowed and owned data. It enables efficient handling of data that may or may not need modification, minimizing allocations while maintaining flexibility.

Cow::Borrowed input, zero-cost needs mutation? checked at call site stays Borrowed no: zero allocation becomes Owned yes: one allocation

What is Cow?

Cow (short for Copy-on-Write) can represent:

  • Borrowed data (&'a B): A reference to existing data, avoiding allocations.
  • Owned data (<B as ToOwned>::Owned): A fully owned copy, allocated only when mutation is required.

Definition (from std::borrow):

pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
{
    Borrowed(&'a B),  // Immutable reference (no allocation)
    Owned(<B as ToOwned>::Owned),  // Owned data (allocated when needed)
}

How It Works:

  • Initially wraps a reference (Borrowed), which is zero-cost.
  • Converts to owned data (Owned) lazily, only when modification is needed.

Example with Cow (Strings)

use std::borrow::Cow;

fn process(input: &str) -> Cow<str> {
    if input.contains("error") {
        Cow::Owned(input.replace("error", ""))  // Allocates new String
    } else {
        Cow::Borrowed(input)  // No allocation
    }
}

fn main() {
    let msg1 = "hello world";  // No allocation
    let msg2 = "error: foo";   // Will allocate when processed

    println!("{}", process(msg1)); // "hello world" (borrowed)
    println!("{}", process(msg2)); // ": foo" (owned)
}

Both calls return the same type, but the returned value holds physically different things:

process("hello world") h e l l o w o r l d caller's bytes, never touched Cow::Borrowed(&str) tag + { ptr, len } — nothing else points back There is no third box: the return value is a window onto memory that already existed. Allocations: 0 process("error: foo") e r r o r : f o o caller's bytes, also never touched Cow::Owned(String) tag + { ptr, len, cap } — a full owner : f o o fresh heap buffer built by replace() Callers deref both the same way and cannot tell which arm they got. The only difference is whether a heap buffer had to be born.

Key Use Cases

1. Optimizing String Operations

Avoid allocations when modifying strings conditionally:

fn to_uppercase(input: &str) -> Cow<str> {
    if input.chars().any(|c| c.is_lowercase()) {
        Cow::Owned(input.to_uppercase())  // Allocates only if needed
    } else {
        Cow::Borrowed(input)
    }
}

Extended Example (checking for digits):

fn to_uppercase_no_digits(input: &str) -> Cow<str> {
    if input.chars().any(|c| c.is_lowercase() || c.is_digit(10)) {
        Cow::Owned(input.to_uppercase().replace(|c: char| c.is_digit(10), ""))
    } else {
        Cow::Borrowed(input)
    }
}

Cow ensures no allocation if the input is already uppercase and digit-free, optimizing read-only paths.

2. API Flexibility

Accept both borrowed and owned data without forcing clones:

fn print(data: Cow<str>) {
    println!("{}", data);
}

fn main() {
    let my_string = String::from("world");
    print(Cow::Borrowed("hello"));  // No allocation
    print(Cow::Owned(my_string));   // Works too
}

This supports &str, String, or other types implementing ToOwned.

3. Zero-Copy Parsing

Common in parsers (e.g., serde), where fields are often unmodified:

struct JsonValue<'a> {
    data: Cow<'a, str>,  // Borrows from input unless modified
}

When to Avoid Cow

  • Always-mutated data: Use String or Vec directly to avoid Cow overhead.
  • Thread-safety: Cow is not thread-safe; use Arc + Mutex for concurrent access.

Performance Implications

Scenario Behavior Allocation Cost
No modification Stays as Borrowed Zero
Modification Converts to Owned One allocation

When Cow pays for itself

Use Cow when:

  • You need to conditionally modify borrowed data.
  • You want to avoid allocations for read-only paths.
  • Your API should accept both &str and String efficiently.

Real-world uses:

  • regex::Match (borrows input strings).
  • serde deserialization.
  • Path manipulation (PathBuf vs. &Path).

Note: Cow works with any ToOwned type (e.g., [u8] → Vec<u8], Path → PathBuf).

Extending the to_uppercase example to digits shows where Cow earns its keep: it only allocates on inputs that actually need rewriting, and returns a borrow for everything else.

  1. Step 17 of 58•6 min
    Understanding the Drop Trait in Rust

    How the Drop trait works, when the destructor runs, and why implementing it by hand is rarer than it looks.

  2. Step 18 of 58•6 min
    Rust Vec::new() vs. with_capacity(): When to Use Each
  3. Step 19 of 58•5 min
    What is the difference between Box<[T]> and Vec<T>?