Skip to content

Step 7: Borrow is the way back

How to learn ToOwned › Step 7 of 10 · back: Step 6 — One blanket impl covers every Clone type · next: Step 8 — The traps are steps 5 and 6 together

Level: 201 · a step on a learning path

One line: type Owned: Borrow<Self> promises that an owned value can lend out its borrowed form — String: Borrow<str>. That is what lets HashMap<String, _>::get take a &str, and what lets a Cow hand you a &str from either arm.

The bound in the trait

pub trait ToOwned {
    type Owned: Borrow<Self>;
    // ...
}

to_owned goes out: &str → String. Borrow comes home: &String → &str. The bound makes the round trip part of the contract, so an impl whose owned type cannot lend out the borrowed one does not compile — E0277, "the trait bound TinyBuf: Borrow<Tiny> is not satisfied" (rustc 1.98.0). You meet that in step 10.

What get asks for

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

With K = String, Q may be str, because String: Borrow<str> — so seats.get("Ada") works without building a String to search with. Borrow also carries a promise the compiler cannot check: the borrowed form must hash and compare exactly as the owned one does. The Borrow page breaks it on purpose.

Cow is built on the pair

pub enum Cow<'a, B: ?Sized + 'a>
where
    B: ToOwned,
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}
  • Reading goes through Deref and allocates nothing: Borrowed hands out its &'a B, and Owned calls .borrow() on the owned value — the promise above, in use.
  • std hands you one when it may have to repair text. CStr::to_string_lossy and String::from_utf8_lossy return Borrowed when the bytes were already valid UTF-8, and Owned only when they had to put U+FFFD in place of bad ones — the checkpoint runs both.
  • Writing is to_mut(). On a Borrowed it calls to_owned(), stores the result, and switches to Owned; on an Owned it copies nothing. The checkpoint runs both.
  • The lifetime stays. A Cow<'a, str> is still tied to 'a after it becomes Owned, because the type does not change when the variant does. into_owned() is how you leave it: it returns the String itself, with no lifetime at all.
  • ToOwned does not promise a heap. Owned is a String for B = str, but for a sized Clone type it is the type itself. The heap comes from String, not from the trait.
  • Nor does a Cow have a separate tag field. Cow<str> is three words, the size of a String: the tag is free.

Why Borrow, and not AsRef

Mechanically, a Cow only needs some way to turn a &String into a &str, and AsRef<str> would do that. What rules AsRef out is the blanket impl from step 6. It sets type Owned = T for every Clone type, so the bound has to hold for T: Borrow<T> — every type lending itself. Borrow has exactly that impl, impl<T: ?Sized> Borrow<T> for T. AsRef does not, and its docs say why: a reflexive impl would overlap with the one that lets AsRef see through references. So i32: AsRef<i32> is E0277 (rustc 1.98.0), and with type Owned: AsRef<Self> the blanket impl could not exist.

The Hash/Eq/Ord agreement that Borrow documents is a different matter. The compiler checks none of it, and ToOwned and Cow work without it. HashMap and BTreeMap are what depend on it, which is where the Borrow page breaks it on purpose.

Checkpoint

Predict before you open the answer. Does seats.get("Ada") compile on a HashMap<String, u32>, and what does it return? Then: after to_mut() on a Cow::Borrowed("Hello World"), is the buffer new — and does a second to_mut() copy again?

The answer

Verified output of borrow_the_way_back.rs — regenerated by tools/run_examples.py, never hand-typed.

Checkpoint. Does seats.get("Ada") compile on a HashMap<String, u32>?
   yes, because String: Borrow<str> -> Some(3)

There and back: to_owned goes out, borrow comes home
   "Ada"     -> String  -> &str   "Ada"
   [3, 1]    -> Vec     -> &[i32] [3, 1]
   notes.txt -> PathBuf -> &Path  "notes.txt"

A Cow<str> lends a &str from either arm
   Cow::Borrowed -> "ballot", Cow::Owned -> "ballot"

std hands you a Cow when it may have to repair the text
   CStr::to_string_lossy("thanksfish")  -> Cow::Borrowed, "thanksfish"
   CStr::to_string_lossy(b"caf\xe9")    -> Cow::Owned, "caf�"
   String::from_utf8_lossy(valid UTF-8) -> Cow::Borrowed, "café"

The write: to_mut calls to_owned only while the Cow is still Borrowed
   before to_mut: Cow::Borrowed, points at the literal: true
   after to_mut:  Cow::Owned, new buffer: true, capacity == length: true
   to_mut again:  Cow::Owned, same buffer, nothing copied: true
   push('!'): no spare capacity, so the String had to reallocate: true
   the literal is untouched: "Hello World", the Cow reads "Hello World!"

The push('!') line is the one a diagram usually gets wrong, and it is not a second clone. Clone-on-write clones at most once: the first to_mut() on a Borrowed Cow, and the second to_mut() copied nothing. Growing is a different cost with a different cause. A String fresh from to_owned has no spare capacity, so the first push must reallocate — which may move the bytes — exactly as it would for any String, Cow or no Cow.

What this step sets up

  • Step 8: the traps — .to_owned() on this Cow is not into_owned(), and on an Rc it copies no text.
  • Step 9: clone_into(&self, target: &mut Self::Owned) writes into the owned half instead of building a new one.
  • Step 10: implementing ToOwned for your own type means implementing Borrow for its owned twin, and the reason a view struct cannot do it.

Go deeper

Read: Borrow: look up an owned key with a borrowed one — what get asks for, why ToOwned needs it — then Cow: borrow until somebody writes, to_mut() is the write and when not to reach for it.

Around it in the library:

Docs: Borrow ↗, whose description is the HashMap case in full · HashMap::get ↗ · Cow ↗ · std::borrow ↗, the module all three live in

Words: Borrow<Q>, Borrow<str> and Cow<'a, B> in the glossary.

Katas: One lookup for every kind of key — three maps searched by their borrowed forms, zero allocations counted. Then pay only when you have to — a Cow that copies only the rows it changes.

Po polsku

type Owned: Borrow<Self> to obietnica drogi powrotnej: to_owned robi z &str String, a Borrow pożycza z String-a z powrotem &str. Bez tej implementacji własne impl ToOwned się nie skompiluje (E0277). Dzięki niej HashMap<String, _>::get przyjmuje &str.

Cow jest zbudowany na tej parze. Odczyt idzie przez Deref i nic nie alokuje: Borrowed oddaje referencję, Owned woła .borrow(). Zapis to to_mut(): na Borrowed woła to_owned() i przełącza się na Owned, na Owned niczego nie kopiuje. Czas życia 'a zostaje w typie także po przejściu na Owned — uwalnia od niego dopiero into_owned(). I uwaga na diagramy: świeżo utworzony String nie ma zapasu pojemności, więc pierwsze push przydziela pamięć od nowa.

Szukaj po polsku: rust Borrow trait HashMap get &str · rust Cow to_mut · kopiowanie przy zapisie


How to learn ToOwned › Step 7 of 10 · back: Step 6 · next: Step 8 — The traps are steps 5 and 6 together