Cow: Clone-on-Write

Reviewed & published by Brayan K

Cow (Clone on Write) is a smart enum that holds either borrowed or owned data, letting a function work with a reference for free and allocate a new owned value only at the moment it actually needs to modify it.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll use std::borrow::Cow to write functions that avoid needless allocation, return Cow<str>, convert with into_owned, and measure exactly when an allocation happens.

What You'll Learn in This Lesson

1️⃣ Borrowed or Owned, On Demand

Cow<str> is an enum with two variants: Cow::Borrowed(&str) wraps a reference with no allocation, and Cow::Owned(String) wraps a freshly allocated value. A function returns Borrowed on the common "nothing changed" path and Owned only when it must produce new data.

use std::borrow::Cow;

// Cow holds EITHER a borrowed reference OR an owned value.
// Here we only allocate a new String when there's something to trim.
fn trimmed(input: &str) -> Cow<'_, str> {
    if input.trim().len() == input.len() {
        // Nothing to change — borrow the original, no allocation.
        Cow::Borrowed(input)
    } else {
        // We must change it — allocate a new owned String.
        Cow::Owned(input.trim().to_string())
    }
}

fn main() {
    let clean = trimmed("hello");        // already trimmed -> Borrowed
    let dirty = trimmed("  spaced  ");   // needs trimming  -> Owned

    println!("clean = '{}'", clean);
    println!("dirty = '{}'", dirty);

    // matches!(...) lets us see which variant we got.
    println!("clean is borrowed? {}", matches!(clean, Cow::Borrowed(_)));
    println!("dirty is owned?    {}", matches!(dirty, Cow::Owned(_)));
}

Because Cow derefs to str, you can print it and call string methods on it directly — callers usually don't even need to know which variant they got.

2️⃣ Allocating Only When You Must

The payoff is in functions that transform text. If the input already meets the requirement, return it borrowed — zero cost. If it needs changing, do the work and return it owned. When you finally need a plain owned value, call into_owned(), which clones only if the Cow was still borrowed.

use std::borrow::Cow;

// Replace spaces with underscores — but only allocate if a space exists.
fn slugify(input: &str) -> Cow<'_, str> {
    if input.contains(' ') {
        Cow::Owned(input.replace(' ', "_"))   // changed -> owned
    } else {
        Cow::Borrowed(input)                  // unchanged -> borrowed
    }
}

fn main() {
    let a = slugify("already_clean");
    let b = slugify("needs cleaning up");

    // into_owned() always gives you a real, owned String,
    // cloning only if the Cow was still borrowed.
    let owned_a: String = a.into_owned();
    let owned_b: String = b.into_owned();

    println!("a -> {}", owned_a);
    println!("b -> {}", owned_b);
}

Here "already_clean" needed no change, so its into_owned() performed a single clone, while "needs cleaning up" was already owned from the replace, so into_owned() was free.

3️⃣ Proving the Allocation Savings

To make the benefit concrete, we can count how many times the Owned variant appears across a batch of inputs. Every unchanged message stays Borrowed and costs nothing; only the messages that actually needed editing allocate.

use std::borrow::Cow;

// Censor a banned word, but avoid allocating for clean messages.
fn censor(message: &str) -> Cow<'_, str> {
    if message.contains("secret") {
        Cow::Owned(message.replace("secret", "*****"))
    } else {
        Cow::Borrowed(message)
    }
}

fn main() {
    let messages = ["hello world", "my secret plan", "all clear"];
    let mut allocations = 0;

    for &m in messages.iter() {
        let result = censor(m);
        if let Cow::Owned(_) = result {
            allocations += 1;   // we only count when we actually allocated
        }
        println!("{}", result);
    }
    println!("allocations made: {}", allocations);
}

Out of three messages, only one contained the banned word, so exactly one allocation happened. With a plain String return type, all three would have allocated — Cow saved two-thirds of the work.

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — return Borrowed when calm, Owned when loud.
use std::borrow::Cow;

fn maybe_shout(input: &str, loud: bool) -> Cow<'_, str> {
    if loud {
        // Uppercasing CHANGES the text, so we must own it.
        Cow::___(input.to_uppercase())     // 👉 the owning variant
    } else {
        // No change needed — just borrow.
        Cow::___(input)                    // 👉 the borrowing variant
    }
}

fn main() {
    println!("{}", maybe_shout("calm", false));
    println!("{}", maybe_shout("loud", true));
}

// ✅ Expected output:
//    calm
//    LOUD

Mini-Challenge: Ensure a URL Prefix

Write a function that prepends a prefix only when it is missing, borrowing the input otherwise. Run it with cargo run and check the output.

// 🎯 MINI-CHALLENGE: ensure a URL prefix without needless allocation.
//
// Write fn ensure_prefix<'a>(input: &'a str, prefix: &str) -> Cow<'a, str>
// that:
//   - returns Cow::Borrowed(input) when input already starts with prefix
//     (no allocation needed), and
//   - returns Cow::Owned(format!("{}{}", prefix, input)) otherwise.
//
// In main, call it on "https://site.com" (already prefixed) and on
// "site.com" (needs the prefix), printing each result.
//
// ✅ Expected output:
//    https://site.com
//    https://site.com

use std::borrow::Cow;

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Cow

ItemMeaning
Cow::Borrowed(x)Wrap a reference, no allocation
Cow::Owned(x)Wrap a freshly allocated value
.into_owned()Get an owned value (clone if borrowed)
.to_mut()Mutable ref, cloning to owned first
Cow<'a, str>Borrowed-or-owned string

🎉 Lesson Complete!

Practice quiz

What does Cow stand for?

  • Copy on Write
  • Container of Wrappers
  • Clone on Write
  • Closure over Writes

Answer: Clone on Write. Cow is Clone on Write: it borrows for free and only allocates a clone at the moment it must modify the data.

Which two variants does the Cow enum have?

  • Borrowed and Owned
  • Some and None
  • Ok and Err
  • Read and Write

Answer: Borrowed and Owned. Cow::Borrowed holds a reference (no allocation); Cow::Owned holds a freshly allocated value.

Which module is Cow imported from?

  • std::rc
  • std::cell
  • std::clone
  • std::borrow

Answer: std::borrow. You write use std::borrow::Cow; to bring it into scope.

On the common path where data is unchanged, which variant should a function return?

  • Cow::Owned
  • Cow::Borrowed
  • Cow::Clone
  • Cow::None

Answer: Cow::Borrowed. Return Cow::Borrowed when nothing changed so you pay no allocation cost.

What does into_owned() do?

  • Always gives a fully owned value, cloning only if it was borrowed
  • Always borrows the data
  • Frees the value
  • Converts Owned back to Borrowed

Answer: Always gives a fully owned value, cloning only if it was borrowed. into_owned() yields an owned value like String; it clones only when the Cow was still Borrowed.

What does Cow::Borrowed take as its argument?

  • An owned String
  • A Vec
  • A reference such as &str
  • A closure

Answer: A reference such as &str. Borrowed wraps a reference (like &str); the Owned variant takes the owned String.

Why return Cow<str> instead of always returning String?

  • It is faster to type
  • It avoids allocating on the unchanged path
  • It prevents borrowing
  • Strings can't be returned

Answer: It avoids allocating on the unchanged path. Always returning String allocates even when nothing changed; Cow borrows for free on the no-change path.

In a censor function over 3 messages where only 1 contains the banned word, how many allocations (Owned variants) happen?

  • 0
  • 2
  • 3
  • 1

Answer: 1. Only the message that actually needed editing allocates, so exactly one Owned variant is produced.

What does Cow's to_mut() do?

  • Returns an immutable reference
  • Gives a &mut to the data, cloning to owned first if needed
  • Deletes the value
  • Converts to a slice

Answer: Gives a &mut to the data, cloning to owned first if needed. to_mut() returns a mutable reference, cloning the borrowed data into an owned value first if necessary.

Roughly how much runtime overhead does Cow add on the common unchanged path?

  • A full heap allocation every time
  • It always doubles memory use
  • Very little — it avoids allocation on the common path
  • It requires a background thread

Answer: Very little — it avoids allocation on the common path. Cow is a small enum; on the unchanged path it skips heap allocation entirely, so it is typically a win.

Continue this course

Frequently asked questions

What does Cow stand for and what is it for?

Cow stands for Clone on Write. It is an enum in std::borrow with two variants: Borrowed, which holds a reference to existing data, and Owned, which holds a freshly allocated value. The point of Cow is to defer allocation: you work with borrowed data for free, and only pay the cost of cloning or allocating a new owned value at the exact moment you actually need to modify it. It is ideal for functions that usually return their input unchanged but occasionally need to produce a modified copy.

When should I return Cow<str> instead of String or &str?

Return Cow<str> when a function sometimes returns its input unchanged and sometimes returns a modified version. If you always returned String, you would allocate even when nothing changed, wasting work. If you returned &str, you could not return a modified version at all, because the new data would have no owner. Cow gives you the best of both: the common no-change path borrows for free, and only the rare modifying path allocates. Trimming, escaping, censoring, and normalizing strings are classic use cases.

What does into_owned do?

into_owned consumes the Cow and always gives you a fully owned value, such as a String. If the Cow was already the Owned variant, it just hands back the value it was holding with no extra work. If the Cow was Borrowed, into_owned clones the borrowed data into a new owned value. This is how you turn a Cow into a plain owned type when you need to store it somewhere with no lifetime constraints. The clone only happens in the borrowed case, so you pay for it only when necessary.

Does Cow add runtime overhead?

Very little. A Cow is just an enum with a one-byte-ish tag plus the borrowed reference or owned value, so checking which variant you have is cheap. The whole design exists to AVOID the bigger cost — heap allocation and copying — on the common path where data is unchanged. So in the typical case Cow is faster than unconditionally allocating, and in the worst case it is about the same as allocating directly. Used where it fits, it is a clear performance win and a clean API at the same time.