po4yka/rust-skills/rust-copy-on-write

Claude Code and agent skill by po4yka/rust-skills

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when choosing between borrowed and owned data at an API boundary (Cow<str>, Cow<[T]>, to_mut, into_owned), including a function that a profile shows allocating a new String or Vec on every call although most inputs come back unchanged, or when clone cost drives a data-structure choice (Arc<str>, the persistent collection crates im, imbl, rpds). Not for other allocation cuts at a named hot spot (use rust-hot-path). Triggers on "copy-on-write", "borrowed or owned", "clone cost", "structural sharing".

Install

npx vskill@latest install po4yka/rust-skills/rust-copy-on-write

SKILL.md

Rust copy-on-write

Every number below was measured on rustc 1.98.1, edition 2024, aarch64-apple-darwin, release profile, with the counting allocator in Measure before you decide. Allocation counts repeat exactly across runs. Times do not; a range is the observed range. Re-measure on your target.

Route the decision to a section

You are about toGo to
Return String where most calls change nothing`Cow` in return position
Chain two or more string or slice transforms`Cow` in argument position
Write cow.to_mut() in front of a method`to_mut()`
Put a Cow field in a struct, or share a value a cache or a task holdsA `Cow` field infects the struct
Write Cow<'_, String> or Cow<'_, Vec<T>>The `Cow` parameter is the borrowed half
Write impl Borrow<..>, or debug a HashMap::get that misses a present keyreferences/borrow-and-toowned.md
Write fn with(&self) -> Self for a version history`Cow` is not structural sharing
Choose between Vec and a persistent collectionPersistent collections
Read a profile that already names __rust_allocThe rust-hot-path skill, when it is installed: capacity, buffer reuse, SmallVec

Verify the decision

ClaimCheckA pass does not prove
The Cow shape saves allocationsThe counting allocator below, on real input, with the Borrowed path in the runThat callers keep the borrow. Read every call site for .into_owned() and .to_string()
No Cow names an owned typecargo clippy (clippy::owned_cow) plus the grep in that sectionAnything about custom ToOwned types. Anything about public items, unless clippy.toml sets avoid-breaking-exported-api = false; the grep covers them
No to_mut() runs before a &self methodThe grep in that section, then a read of each hitThat each to_mut() sits inside the branch that writes
A Borrow<X> key finds its entriesA test that compares hash_one of the key with hash_one of its borrowed form on one RandomState, for a key in each letter case that eq folds. Then a get through the borrowed formA correct contract, from a get test alone: it passes about 1 run in 150 with a broken contract. A lookup through the owned key proves nothing
The collection crate's resolved tree is cleancargo deny --config deny.toml --locked check advisories with unsound = "all" in deny.toml, or cargo audit --deny warnings, on the lockfileA clean tree, if you drop the setting: default cargo-deny misses the transitive sized-chunks unsound advisory in an im tree, and default cargo audit exits 0 on every informational advisory. A failure does not prove that your code reaches the flagged API

Measure before you decide

Every rule here is a hit-rate rule. A hit rate is a property of your workload, not of the type. Put this allocator in a bench binary, run your real input through both shapes, and compare the counts.

rust,run
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
pub static ALLOCS: AtomicUsize = AtomicUsize::new(0);
pub static BYTES: AtomicUsize = AtomicUsize::new(0);
pub struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
BYTES.fetch_add(layout.size(), Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
BYTES.fetch_add(new_size, Ordering::Relaxed);
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static A: Counting = Counting;
fn main() {
let (a0, b0) = (ALLOCS.load(Ordering::Relaxed), BYTES.load(Ordering::Relaxed));
let cap = std::hint::black_box(Vec::<u32>::with_capacity(1000)).capacity();
let allocs = ALLOCS.load(Ordering::Relaxed) - a0;
let bytes = BYTES.load(Ordering::Relaxed) - b0;
println!("allocs={allocs} bytes={bytes}");
assert_eq!((allocs, bytes, cap), (1, 4000, 1000));
}

Run it in release. dhat gives the same counts with less code; the rust-performance skill covers it, when it is installed.

Cow in return position

Cow<'_, str> pays only on the branch that changes the data. Measured over 1000 path strings where 5% need a rewrite, generated by (0..1000).map(|i| if i % 20 == 0 { format!("path\\{i}") } else { format!("path/{i}") }). The lengths sum to 7890 bytes:

Return shapeAllocationsBytes
String10007890
Cow<'_, str>50394
Cow<'_, str>, and the caller writes .into_owned()10007890

The third row is the rule. Cow in return position wins only when the caller can hold the borrow. The moment a call site writes .into_owned() or .to_string(), the count returns to the String figure, and the Cow is a branch plus a lifetime parameter for nothing.

rust,run
use std::borrow::Cow;
fn normalize(path: &str) -> Cow<'_, str> {
if path.contains('\\') {
Cow::Owned(path.replace('\\', "/"))
} else {
Cow::Borrowed(path)
}
}
fn main() {
let inputs = ["a/b", "a\\b"];
// One allocation over two inputs, not two.
let total: usize = inputs.iter().map(|p| normalize(p).len()).sum();
assert_eq!(total, 6);
// .into_owned() at the call site puts the allocation straight back.
let owned: Vec<String> = inputs.iter().map(|p| normalize(p).into_owned()).collect();
assert_eq!(owned.len(), 2);
}

into_owned() costs 0 allocations on a Cow::Owned and 1 on a Cow::Borrowed. Call it once at the boundary where the source buffer dies, not inside the loop that produced the Cow.

Two checks before you convert a signature:

  • Measure the branch split. Cow allocates exactly (modification rate x N) times, and

String always N: 0 at 0%, 250 at 25%, 600 at 60%, 1000 at 100%. It saves nothing only at 100%. The 20x above comes from a 5% rate.

  • Read the call sites. One .into_owned() in the only caller cancels the whole change.

Cow is free in size. Measured on aarch64: &str 16 bytes, String 24, Cow<'_, str> 24, Cow<'_, [u8]> 24. Enum layout is an unspecified implementation detail, and Rust 1.97 changed the layout of some enums. If you depend on the size, assert it in a form that holds on every target: const _: () = assert!(size_of::<Cow<'_, str>>() == size_of::<String>());.

Cow in argument position

A single Cow return is a small win. Cow -> Cow through a chain is a large one, because each stage passes its input through untouched instead of re-allocating between stages. Measured over the same 1000 inputs with three chained stages. The String row runs the same three stages with fn(&str) -> String signatures:

ChainAllocationsBytes
Cow<'a, str> -> Cow<'a, str>, 3 stages50394
String -> String, 3 stages300023670

Only to_slashes allocates on this input set. The other two stages pass their input through, so the chain costs what one stage costs. The String chain pays 7890 bytes per stage whether the stage changes anything or not.

rust,run
use std::borrow::Cow;
// Reborrowing out of an Owned value does not compile, so re-own the tail.
fn strip_dot(s: Cow<'_, str>) -> Cow<'_, str> {
match s.strip_prefix("./") { Some(r) => Cow::Owned(r.to_string()), None => s }
}
fn to_slashes(s: Cow<'_, str>) -> Cow<'_, str> {
if s.contains('\\') { Cow::Owned(s.replace('\\', "/")) } else { s }
}
fn drop_trailing(s: Cow<'_, str>) -> Cow<'_, str> {
if s.ends_with('/') { Cow::Owned(s[..s.len() - 1].to_string()) } else { s }
}
fn main() {
let clean = drop_trailing(to_slashes(strip_dot(Cow::Borrowed("a/b"))));
assert!(matches!(clean, Cow::Borrowed("a/b"))); // zero allocations
assert_eq!(drop_trailing(to_slashes(strip_dot(Cow::Borrowed("a\\b/")))), "a/b");
}

Take Cow<'a, str> by value in a chain stage. A stage that takes &Cow<'a, str> must clone an Owned input only to pass it through, which is the cost the chain exists to avoid.

to_mut() is for &mut self methods only

to_mut() on a Cow::Borrowed clones the borrowed data into an owned value, then returns &mut. A &self method called on that &mut allocates a second time and drops the first.

This form allocates twice:

rust,run
use std::borrow::Cow;
fn main() {
let mut cow: Cow<'_, str> = Cow::Borrowed("cow says moo");
// to_mut() clones into a String, then str::replace builds a second String
// and the first is dropped: 2 allocations, 24 bytes.
let shout = cow.to_mut().replace("moo", "MOO");
assert_eq!(shout, "cow says MOO");
}

This form allocates once. Deref reaches str::replace with no to_mut() at all:

rust,run
use std::borrow::Cow;
fn main() {
let cow: Cow<'_, str> = Cow::Borrowed("cow says moo");
let shout = cow.replace("moo", "MOO"); // 1 allocation, 12 bytes
assert_eq!(shout, "cow says MOO");
assert!(matches!(cow, Cow::Borrowed(_))); // still shareable
// The second effect: to_mut() flips the value to Owned even when nothing
// is written. Every later consumer has lost the zero-copy property.
let mut untouched: Cow<'_, str> = Cow::Borrowed("unchanged");
let _ = untouched.to_mut();
assert!(matches!(untouched, Cow::Owned(_)));
}

Use to_mut() only for a method that takes &mut self: push_str, Vec::push, sort. to_mut().push_str("!") on a borrowed 13-byte string costs 2 allocations and 39 bytes: to_mut() allocates 13, then push_str reallocates to 26. When you know the final length, String::with_capacity(s.len() + 1) followed by push_str and push costs 1 allocation and 14 bytes. Keep the call inside the branch that writes, not above it.

to_mut() on an already-Owned value is free, so a benchmark that starts from Cow::Owned shows no difference and hides both defects. Benchmark the Borrowed path.

Grep for the misuse. Every hit needs a check that the method takes &mut self:

bash
rg '\.to_mut\(\)\.' --type rust -n

A Cow field infects the struct with a lifetime

struct Header<'a> { name: Cow<'a, str> } is a borrowing struct. The lifetime spreads to every signature that mentions it, exactly like a &'a mut T field. The rust-discipline skill, when it is installed, covers lifetime infection in general. Without it, give a long-lived struct an owned field (String) or a generic parameter, so it carries no lifetime.

Two failures arrive together:

rust,compile_fail,E0515,E0521
use std::borrow::Cow;
struct Header<'a> {
name: Cow<'a, str>,
}
// error[E0515]: cannot return value referencing local variable `raw`
fn build() -> Header<'static> {
let raw = String::from("content-type");
Header { name: Cow::Borrowed(&raw) }
}
// error[E0521]: borrowed data escapes outside of function
// argument requires that `'1` must outlive `'static`
fn store(out: &mut Vec<Header<'static>>, raw: &str) {
out.push(Header { name: Cow::Borrowed(raw) });
}

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
2
Repository forks
1
7-day trend score
5

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/po4yka/rust-skills/rust-copy-on-write/badge)](https://verified-skill.com/skills/po4yka/rust-skills/rust-copy-on-write)

Something wrong with this skill or its page? Tell us.