Step 8: The traps are steps 5 and 6 together¶
How to learn ToOwned › Step 8 of 10 · back: Step 7 — Borrow is the way back · next: Step 9 — clone_into refills instead of allocating
Level: 201 · a step on a learning path
One line: Rc<String> is Clone, so .to_owned() on it is Rc::clone — a pointer and a count. Cow is Clone, so .to_owned() on it is another Cow in the same variant. Neither is a bug in ToOwned: both are lookup finding the outer type first.
Rc: the outer type is the first candidate¶
shared: Rc<String>, and shared.to_owned():
- First candidate,
Rc<String>by value:to_ownedtakes&self, so nothing. - Second,
&Rc<String>:Rc<String>isClone, so the blanket impl from step 6 has ato_ownedwith exactly that receiver — match.
The String inside is never considered, because the search stopped a rung above it. What you get is Rc::clone: the same allocation, and a strong count one higher. The text is not copied.
What was meant is a call whose first candidate is the String: (*shared).to_owned(), String::clone(&shared), or shared.as_str().to_owned().
The reverse habit exists for the same reason. When you do want the pointer, the std::rc docs recommend writing Rc::clone(&shared) rather than shared.clone(): both compile to the same call, but only the first tells a reader, at the call site, that no text is being copied.
Cow: .to_owned() is not .into_owned()¶
Cow<'_, str> — the type step 7 took apart — is Clone too. So cow.to_owned() clones the Cow — a Borrowed stays Borrowed, still tied to the original borrow's lifetime, and nothing about it is more owned than before.
What was meant is cow.into_owned(), which returns the String: allocating if the Cow was Borrowed, and just unwrapping it if it was already Owned. Clippy's suspicious_to_owned exists for this one call.
Why Cow is Clone at all¶
Taking Clone away from Cow would remove this trap, and a great deal more with it. A struct that holds a Cow — a token borrowing from its source line, a config value, a parsed record — can only #[derive(Clone)] if every field is Clone. And cloning a Cow does the right thing for a value: a Borrowed one copies the reference, an Owned one copies the text. The checkpoint's last lines run both. The trap is not Cow's Clone but the blanket impl turning it into a to_owned, which is why the fix is a lint, not a missing impl.
The shape to recognise¶
Whenever the value in your hand is a wrapper that is Clone — Rc<T>, Arc<T>, Cow<'_, B>, Box<T>, or a &T from step 6 — .to_owned() and .clone() act on the wrapper. The type written in the variable's annotation is the first candidate, not the type you are thinking about.
Checkpoint¶
Predict before you open the answer. After shared.to_owned() on an Rc<String>, what is the strong count? After .to_owned() on a Cow::Borrowed, which variant do you hold?
The answer
Verified output of to_owned_traps.rs — regenerated by tools/run_examples.py, never hand-typed.
Checkpoint. Strong count after Rc::to_owned? Variant after Cow::to_owned?
Rc::strong_count = 2, same allocation: true
Cow::Borrowed(..).to_owned() -> Cow::Borrowed
What was meant: name the inner type, or ask the Cow for its owned form
(*shared).to_owned() -> alloc::string::String, same bytes as the Rc's: false
cow.into_owned() -> alloc::string::String "ballot"
Why Cow is Clone at all: so that what holds one can derive Clone
Borrowed token cloned: same bytes as the line: true
Owned token cloned: same bytes as before: false
What this step sets up¶
- Step 9: when a copy is what you want, how to make it without a new allocation.
- After the steps: both traps copy no text, which is why "
ToOwnedalmost always allocates" is the wrong mental model.
Go deeper¶
Read: ToOwned — the trap that blanket impl sets, then Rc: the clone that copies a pointer — what the clone copied: nothing and Rc::clone(&x) and x.clone() are the same call.
Around it in the library:
- The third owned form — the trap:
.to_owned()on anRcclones the pointer, withRc<str> Cow: borrow until somebody writes — the type, in full- What a clone costs — the call site cannot tell you which one you got
- Sharing across threads, where the same trap is spelled
Arc - Method resolution, if the numbered search above went by too fast
Docs: Rc ↗ · Cow::into_owned ↗, the method you wanted instead · clippy's suspicious_to_owned ↗
Words: Cow<'a, B> in the glossary.
Katas: Predict the count four times — one roster shared by three tallies, and the edge that leaks. Then freeze a candidate column three ways — ending on the .to_owned() that clones a pointer instead of the text.
Po polsku¶
To nie są błędy ToOwned, tylko kroki 5 i 6 naraz. Rc<String> jest Clone, więc shared.to_owned() znajduje implementację zbiorczą już dla &Rc<String> — i robi Rc::clone: ten sam przydział pamięci, licznik o jeden większy, tekst nieskopiowany. Żeby dostać nowy String, trzeba zacząć od String-a: (*shared).to_owned().
Tak samo Cow: cow.to_owned() klonuje samego Cow, a Borrowed zostaje Borrowed. Chodziło o cow.into_owned(). Zasada ogólna: gdy w ręku jest opakowanie, które jest Clone, .to_owned() działa na opakowaniu.
Szukaj po polsku: rust Rc to_owned · rust Cow to_owned vs into_owned · clippy suspicious_to_owned
How to learn ToOwned › Step 8 of 10 · back: Step 7 · next: Step 9 — clone_into refills instead of allocating