Advanced borrowing — Cow & AsRef

Advanced borrowing — Cow & AsRef

Prerequisites

The borrow-bases, lifetimes and generics courses.

1. The owned / borrowed pairs

Owned (allocates, mutable)Borrowed (view, no allocation)
String&str
Vec<T>&[T]
PathBuf&Path
OsString&OsStr
Box<T>&T

Each pair follows the same principle: the owned type contains the data; the borrowed type is a view on data that belongs to someone else. You go from one to the other:

let owned: String = String::from("abc");
let view: &str = &owned;               // owned -> borrowed: free
let copy: String = view.to_owned();    // borrowed -> owned: allocation

The traits in this course generalise these conversions.


2. `AsRef<T>` and `AsMut<T>`

2.1 The trait

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

"I can give you a &T, without copying." String, &str, Box<str> and Cow<str> all implement AsRef<str>.

2.2 Accepting several types as a parameter

fn count_words<S: AsRef<str>>(text: S) -> usize {
    text.as_ref().split_whitespace().count()
}

count_words("one two");                        // &str
count_words(String::from("one two three"));    // String
count_words(&String::from("a"));               // &String

The standard library uses it everywhere for paths:

use std::path::Path;

fn exists<P: AsRef<Path>>(path: P) -> bool {
    path.as_ref().exists()
}

exists("config.toml");                    // &str
exists(String::from("config.toml"));      // String
exists(Path::new("config.toml"));         // &Path
exists(std::path::PathBuf::from("a"));    // PathBuf

std::fs::read_to_string, File::open and Command::new all accept impl AsRef<Path> or impl AsRef<OsStr>.

2.3 Collections of strings

fn join_all<S: AsRef<str>>(parts: &[S]) -> String {
    parts.iter().map(|p| p.as_ref()).collect::<Vec<_>>().join(", ")
}

join_all(&["a", "b"]);
join_all(&[String::from("a"), String::from("b")]);

Without AsRef, you would need two functions, or you would have to convert the Vec<String> into a Vec<&str> before each call.

2.4 `AsMut<T>`

fn zero_out<B: AsMut<[u8]>>(mut buffer: B) {
    buffer.as_mut().fill(0);
}

let mut v = vec![1u8, 2, 3];
zero_out(&mut v);
let mut t = [9u8; 4];
zero_out(&mut t);

2.5 Implementing `AsRef` for your own type

struct Email(String);

impl AsRef<str> for Email {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

count_words(Email("[email protected]".into()));

3. `Borrow<T>` and `BorrowMut<T>`

3.1 The trait

pub trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}

The signature is the same as AsRef. The difference is a promise: Borrow guarantees that the borrowed form compares, hashes and orders exactly like the owned form.

3.2 Why it is useful: map lookups

use std::collections::HashMap;

let mut m: HashMap<String, u32> = HashMap::new();
m.insert("ada".to_string(), 36);

let age = m.get("ada");          // we search with a &str, without creating a String

The signature of get:

pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
    K: Borrow<Q>,
    Q: Hash + Eq,

String: Borrow<str> lets you search with a &str. The promise of Borrow guarantees that "ada" hashed as a &str gives the same result as String::from("ada"): otherwise the lookup would fail silently.

3.3 Keeping the promise

// A case-insensitive key
#[derive(PartialEq, Eq, Hash)]
struct LowerKey(String);

// DO NOT: impl Borrow<str> for LowerKey
// "ADA" and "ada" are equal as LowerKey but different as &str:
// a lookup by &str would give inconsistent results.

If the equality or hashing of your type differs from that of the borrowed form, implement AsRef, not Borrow.

3.4 Lookup with a composite type

use std::collections::HashSet;

let mut s: HashSet<Vec<u8>> = HashSet::new();
s.insert(vec![1, 2]);
s.contains(&[1u8, 2][..]);      // Vec<u8>: Borrow<[u8]>: we search with a slice

3.5 `BorrowMut<T>`

The mutable equivalent, rarely used directly.


4. `ToOwned`

4.1 The trait

pub trait ToOwned {
    type Owned: Borrow<Self>;
    fn to_owned(&self) -> Self::Owned;
}

Clone only works from &T to T. ToOwned generalises it: from a borrowed form to its owned form, even if the types differ.

let s: &str = "abc";
let a: String = s.to_owned();          // str -> String
let t: &[i32] = &[1, 2];
let b: Vec<i32> = t.to_owned();        // [i32] -> Vec<i32>
let p = std::path::Path::new("a");
let c: std::path::PathBuf = p.to_owned();

4.2 `to_owned`, `to_string`, `String::from`, `into`

let s = "text";
let a = s.to_owned();        // ToOwned: generic, the most direct
let b = s.to_string();       // ToString (via Display): works for any displayable type
let c = String::from(s);     // From
let d: String = s.into();    // Into

All four give the same result for a &str, at the same cost. to_string is the only one that also works for 42.to_string().


5. `AsRef`, `Borrow`, `Into`, `Deref`: which one to choose

TraitPromisesCostUse as a parameter
AsRef<T>"I can give a &T"nonethe function reads: fn f(p: impl AsRef<Path>)
Borrow<T>same + same equality and hashingnonecollection keys (get, contains)
Into<T>"I can become a T"may allocatethe function keeps the value: fn new(name: impl Into<String>)
Deref"I am a pointer to T"nonenot used as a bound: automatic conversion

5.1 The same function with each form

struct User { name: String }

// &str: simple, accepts &String through automatic conversion (Deref), not String
fn greet_v1(name: &str) -> String { format!("Hello {name}") }

// AsRef: also accepts a String by value
fn greet_v2(name: impl AsRef<str>) -> String { format!("Hello {}", name.as_ref()) }

// Into<String>: the function needs an owned String
fn create(name: impl Into<String>) -> User { User { name: name.into() } }

fn main() {
    let s = String::from("Ada");
    greet_v1(&s);
    greet_v1("Linus");
    greet_v2(s.clone());
    create("Grace");         // allocates a String
    create(s);               // no copy: the String is moved
}

Simple rule:

  • the function reads a string: &str (or impl AsRef<str> if the API must accept many forms);
  • the function stores a string: impl Into<String> (or String);
  • the function does a lookup in a collection: a Borrow bound.

6. `Cow<'a, B>`: borrow or own as needed

6.1 Definition

pub enum Cow<'a, B: ?Sized + ToOwned + 'a> {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

Cow (clone on write) holds either a reference, or an owned value. Cow<'a, str> is either a &'a str or a String.

6.2 The problem it solves

// Always allocate: a String is created even when nothing changes
fn no_spaces_v1(s: &str) -> String {
    s.replace(' ', "_")
}

// Allocate only if necessary
use std::borrow::Cow;

fn no_spaces_v2(s: &str) -> Cow<'_, str> {
    if s.contains(' ') {
        Cow::Owned(s.replace(' ', "_"))   // a change is needed: we allocate
    } else {
        Cow::Borrowed(s)                   // nothing to change: we return the input as is
    }
}

If most inputs have no space, the second version almost never allocates.

6.3 A `Cow` is used like the value

let r = no_spaces_v2("hello big world");
println!("{r}");                 // Display
println!("{}", r.len());         // Deref to str: all the str methods
let s: &str = &r;

7. All the ways to create and read a `Cow`

7.1 Creating

use std::borrow::Cow;

let a: Cow<str> = Cow::Borrowed("text");
let b: Cow<str> = Cow::Owned(String::from("text"));
let c: Cow<str> = "text".into();                  // From<&str>: Borrowed
let d: Cow<str> = String::from("text").into();    // From<String>: Owned
let e: Cow<[i32]> = Cow::from(&[1, 2][..]);
let f: Cow<[i32]> = Cow::from(vec![1, 2]);

7.2 Reading and converting

let mut c: Cow<str> = Cow::Borrowed("abc");

let length = c.len();                     // via Deref
let view: &str = c.as_ref();
let is_borrowed = matches!(c, Cow::Borrowed(_));

c.to_mut().push('!');                     // if Borrowed: copies first, then modifies; c becomes Owned
let owned: String = c.into_owned();       // String: no copy if already Owned
MethodEffect
Deref (*c, c.len())read, in every case, without copying
to_mut()&mut to the owned value; copies the first time if Borrowed
into_owned()the owned value; copies only if Borrowed
clone()if Borrowed, copies the reference; if Owned, copies the value

7.3 Modify only if necessary

use std::borrow::Cow;

fn normalize(input: &str) -> Cow<'_, str> {
    let mut r = Cow::Borrowed(input);
    if r.starts_with(' ') || r.ends_with(' ') {
        r = Cow::Owned(r.trim().to_string());
    }
    if r.contains("  ") {
        let simplified = r.split_whitespace().collect::<Vec<_>>().join(" ");
        r = Cow::Owned(simplified);
    }
    r
}

8. `Cow` in practice

8.1 HTML escaping

use std::borrow::Cow;

fn escape(s: &str) -> Cow<'_, str> {
    if !s.contains(['<', '>', '&', '"', '\'']) {
        return Cow::Borrowed(s);               // the common case: no special character
    }
    let mut r = String::with_capacity(s.len() + 8);
    for c in s.chars() {
        match c {
            '<' => r.push_str("&lt;"),
            '>' => r.push_str("&gt;"),
            '&' => r.push_str("&amp;"),
            '"' => r.push_str("&quot;"),
            '\'' => r.push_str("&#x27;"),
            _ => r.push(c),
        }
    }
    Cow::Owned(r)
}

This is the principle of template engines (Tera escapes every variable printed with {{ }}): most texts have nothing to escape.

8.2 Messages: `Cow<'static, str>`

use std::borrow::Cow;

fn message(code: u16, detail: Option<&str>) -> Cow<'static, str> {
    match (code, detail) {
        (404, None) => Cow::Borrowed("Page not found"),
        (404, Some(d)) => Cow::Owned(format!("Page not found: {d}")),
        (_, _) => Cow::Borrowed("Error"),
    }
}

Cow<'static, str> is either a literal (no allocation, lives for the whole program) or a built String. It is an ideal type for messages, field names and configuration keys.

8.3 In a struct

use std::borrow::Cow;

struct Request<'a> {
    path: Cow<'a, str>,
}

impl<'a> Request<'a> {
    fn from_path(path: &'a str) -> Self {
        Self { path: Cow::Borrowed(path) }
    }

    fn normalize(&mut self) {
        if self.path.ends_with('/') && self.path.len() > 1 {
            let trimmed = self.path.trim_end_matches('/').to_string();
            self.path = Cow::Owned(trimmed);
        }
    }

    fn into_static(self) -> Request<'static> {
        Request { path: Cow::Owned(self.path.into_owned()) }   // detach from the source
    }
}

8.4 `Cow` and serde

#[derive(serde::Deserialize)]
struct Article<'a> {
    #[serde(borrow)]
    title: Cow<'a, str>,     // borrows from the source JSON when no escape has to be decoded
}

With #[serde(borrow)], a Cow<str> field borrows the JSON text directly if it contains no escape sequence (\n, \"), and allocates otherwise.

8.5 When not to use `Cow`

Cow adds a lifetime and an internal match. For a function that always modifies its input, return a String directly. For a function that never modifies it, return a &str. Cow is only worth it if both cases exist and the unmodified case is frequent.


9. Techniques to avoid copies

9.1 Borrow in loops

let users = vec![String::from("ada"), String::from("linus")];

// Copies each name
let names: Vec<String> = users.iter().cloned().collect();

// Borrows: no copy, valid as long as users exists
let views: Vec<&str> = users.iter().map(String::as_str).collect();

9.2 Return a reference rather than a copy

struct Profile { name: String }

impl Profile {
    fn name(&self) -> &str { &self.name }                 // no clone
    fn name_copy(&self) -> String { self.name.clone() }   // only if the caller must own it
}

9.3 Write into a provided buffer

use std::fmt::Write;

fn write_line(output: &mut String, name: &str, age: u32) {
    let _ = writeln!(output, "{name};{age}");     // appends to the existing buffer
}

let mut csv = String::with_capacity(1024);
for (n, a) in [("ada", 36), ("linus", 54)] {
    write_line(&mut csv, n, a);                    // a single allocation for the whole file
}

9.4 Move instead of clone

fn register(name: String) {}

let name = String::from("ada");
// register(name.clone());       // useless if name is no longer used
register(name);                   // move: no copy

clippy::redundant_clone flags clones whose source is not used afterwards.

9.5 Take instead of clone

struct Form { errors: Vec<String> }

impl Form {
    fn drain_errors(&mut self) -> Vec<String> {
        std::mem::take(&mut self.errors)        // takes the Vec, leaves an empty Vec
    }
}

10. Examples in Runique

10.1 Translations: `Cow<'static, str>`

// runique/src/utils/trad/switch_lang.rs
pub fn t(key: &str) -> Cow<'static, str> { /* ... */ }
pub fn tf<T: Display>(key: &str, args: &[T]) -> String { /* ... */ }

t returns a Cow<'static, str>: the translation of a key without parameters is a fixed text, returned without allocation. tf fills in parameters ({}): the result is necessarily new, hence a String. Both signatures follow the rule of section 8.5 exactly.

10.2 `effective_key` of the `LoginGuard`

// runique/src/auth/guard.rs
pub fn effective_key<'a>(username: &'a str, ip: &str) -> std::borrow::Cow<'a, str> {
    let username = username.trim();
    if username.is_empty() {
        std::borrow::Cow::Owned(format!("anonym:{ip}"))
    } else {
        std::borrow::Cow::Owned(username.to_lowercase())
    }
}

The key of the login attempt counter is normalised (trim then to_lowercase): admin, Admin and admin share a single counter. Without this, changing the case would bypass the lockout. The normalisation allocates, so both branches return Owned; the Cow signature leaves room to return Borrowed (for example for an input already in lowercase) without changing the callers.

10.3 Flexible parameters

Runique constructors often accept impl Into<String> for names and labels: the user passes a literal or a String without explicit conversion, and the value is stored owned (section 5).


11. The same function written in several ways

Capitalise the first letter of a word.

use std::borrow::Cow;

// 1. Always a new String
fn capitalize_v1(s: &str) -> String {
    let mut c = s.chars();
    match c.next() {
        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
        None => String::new(),
    }
}

// 2. Cow: no allocation if the first letter is already uppercase
fn capitalize_v2(s: &str) -> Cow<'_, str> {
    match s.chars().next() {
        Some(f) if f.is_uppercase() || !f.is_alphabetic() => Cow::Borrowed(s),
        Some(_) => Cow::Owned(capitalize_v1(s)),
        None => Cow::Borrowed(s),
    }
}

// 3. In place, in the caller's String (ASCII only: same length in bytes)
fn capitalize_v3(s: &mut String) {
    if let Some(first) = s.get_mut(0..1) {
        first.make_ascii_uppercase();
    }
}

// 4. Generic input, owned output
fn capitalize_v4(s: impl AsRef<str>) -> String {
    capitalize_v1(s.as_ref())
}
VersionAllocationWhen
1alwayssimple, when the result must be stored
2only if necessarycalled very often on data that is already correct
3neverthe caller already owns the String and no longer needs the original
4alwaysAPI that accepts &str, String, Cow...

12. Exercises

Exercise 1 — `AsRef`

Write extension(path: impl AsRef<Path>) -> Option<String> that returns the extension in lowercase, and call it with a &str, a String and a PathBuf.

// Solution:
use std::path::{Path, PathBuf};

fn extension(path: impl AsRef<Path>) -> Option<String> {
    path.as_ref().extension()?.to_str().map(str::to_lowercase)
}

fn main() {
    extension("photo.JPG");
    extension(String::from("doc.pdf"));
    extension(PathBuf::from("archive.tar.gz"));
}

Exercise 2 — `Cow`

Write strip_accents(s: &str) -> Cow<str> that replaces é, è, ê with e and à with a, without allocating when there is nothing to replace.

// Solution:
use std::borrow::Cow;

fn strip_accents(s: &str) -> Cow<'_, str> {
    if !s.contains(['é', 'è', 'ê', 'à']) {
        return Cow::Borrowed(s);
    }
    Cow::Owned(
        s.chars()
            .map(|c| match c {
                'é' | 'è' | 'ê' => 'e',
                'à' => 'a',
                other => other,
            })
            .collect(),
    )
}

assert!(matches!(strip_accents("rust"), Cow::Borrowed(_)));
assert_eq!(strip_accents("été"), "ete");

Exercise 3 — lookup key

A HashMap<String, u32> is queried thousands of times with &str values. Write the lookup without creating a String.

// Solution:
use std::collections::HashMap;

fn age(m: &HashMap<String, u32>, name: &str) -> Option<u32> {
    m.get(name).copied()       // String: Borrow<str>
}

13. Cheat sheet

fn f(x: impl AsRef<str>)        // read: accepts &str, String, &String, Cow<str>
fn f(x: impl AsRef<Path>)       // paths
fn f(x: impl Into<String>)      // store: accepts &str (allocates) and String (moves)
map.get(&borrowed_key)          // thanks to K: Borrow<Q>
x.to_owned()                    // borrowed -> owned (&str -> String, &[T] -> Vec<T>)

Cow::Borrowed(&x)   Cow::Owned(v)   "x".into()   String::from("x").into()
c.to_mut()          c.into_owned()  *c / c.len()
fn f(s: &str) -> Cow<'_, str>   // allocates only if necessary
Cow<'static, str>               // literal or built String

Key points

  • Every owned type has its borrowed view: String/str, Vec<T>/[T], PathBuf/Path.
  • AsRef to read several forms, Into to store, Borrow for collection lookups.
  • Borrow promises the same equality and hashing: do not implement it if that is not true.
  • Cow allocates only when a modification is needed; useful if the unmodified case is frequent.
  • Cow<'static, str>: fixed or built text, with no lifetime to propagate.
  • Moving, borrowing and taking (mem::take) avoid most clone calls.