Skip to content

← Collections, Text and First Iterators step 18 of 21

Hard Framework

Cow in API design: impl Into<Cow<str>>

This one is about designing with Cow, not just returning one. Three pieces:

pub struct Message<'a> { text: Cow<'a, str> }

impl<'a> Message<'a> {
    pub fn new(text: impl Into<Cow<'a, str>>) -> Self
}

pub fn render(template: &str, vars: Vec<(String, String)>) -> Cow<'_, str>
pub fn build(inputs: Vec<String>) -> Vec<(String, bool)>

render substitutes {name} placeholders from vars:

  • a {name} whose key is in vars is replaced by its value;
  • a {name} whose key is absent is left verbatim, braces and all;
  • an unclosed { and everything after it is copied through as-is;
  • if the template contains no { at all, return Cow::Borrowed(template) and allocate nothing. Otherwise return Cow::Owned, even if nothing was actually substituted.

build exercises Message. For each input string s: if s is empty, construct Message::new("(empty)") from a string literal; otherwise construct Message::new(format!("<{s}>")) from an owned String. Return (text, was_borrowed) for each. The literal ones must come back borrowed; the formatted ones owned. That pair of assertions is the whole point of the parameter type.

Error one: E0106, and the cascade behind it

pub struct Message {
    text: Cow<str>,
}
error[E0106]: missing lifetime specifier

Cow<'a, B> has a lifetime parameter because its Borrowed variant holds a reference, and a reference always needs to say how long the thing it points at lives. There is no elision rule for struct fields — elision only works where the compiler can copy a lifetime from somewhere obvious, and a struct definition has no “somewhere obvious”.

So the fix is struct Message<'a> { text: Cow<'a, str> }, and that is the cost you are buying into. The struct is now generic over a lifetime, which means:

impl<'a> Message<'a> { ... }
fn handle(m: Message<'_>) { ... }
struct Inbox<'a> { messages: Vec<Message<'a>> }   // and now Inbox is too
fn load<'a>(raw: &'a str) -> Inbox<'a> { ... }    // and now every function is

The 'a propagates outward through every type that contains a Message and every signature that mentions one. This is the honest reason people abandon Cow halfway through a refactor: they add it to one leaf type and then spend an afternoon threading a lifetime through forty signatures.

Sometimes String is just better. If the struct is long-lived, stored in a collection, sent across a thread, or held in a global, it cannot borrow from a short-lived buffer anyway, and Cow buys you nothing but ceremony. Use Cow in a struct when the struct is a short-lived view over data somebody else owns — a parsed header, a request being processed, a row being formatted — and the borrowed case is the common one.

Error two: E0308, and why impl Into<Cow<'a, str>>

The starter’s constructor takes a String:

pub fn new(text: String) -> Self
error[E0308]: mismatched types
  Message::new("(empty)")
               ^^^^^^^^^ expected `String`, found `&str`

You could “fix” that with .to_string() at the call site — and you would have forced an allocation on a caller who already had a perfectly good &'static str. The API would be quietly taxing every user who has the cheap thing.

The fix is to accept whichever they have:

pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
    Message { text: text.into() }
}

std provides From<&'a str> for Cow<'a, str> (producing Borrowed) and From<String> for Cow<'a, str> (producing Owned). So:

Message::new("hello")            // Borrowed, zero allocations
Message::new(format!("{x}"))     // Owned, the allocation you already paid for
Message::new(some_cow)           // passes straight through

One function, three call shapes, no conversion anywhere. This is how ergonomic Rust APIs avoid forcing allocations on their callers, and it is not something you arrive at by accident — you have to know the pattern exists.

::: question impl Into<Cow<'a, str>> versus the more familiar impl AsRef<str>. How do you choose? The decision rule is one question: do I sometimes need to store it?

AsRef<str> says “I will look at your string and I promise not to keep it”. It accepts &str, String, Cow, &String, Box<str> — a wider set — and it is the right bound for a function that reads and returns something else. fn word_count(s: impl AsRef<str>) -> usize should be AsRef.

Into<Cow<'a, str>> says “I may keep it, and I would like to avoid allocating if you already own it or if it will outlive me”. It is the right bound for constructors and for anything that stores its argument. If you took AsRef<str> and then had to store it, you would be forced to call .as_ref().to_owned() — allocating even when the caller handed you a String they were about to throw away.

So: reading only, AsRef. Storing sometimes, Into<Cow>. Storing always, just take String and let the caller decide. :::

render: the borrowed fast path

The specification says “no { in the template means Cow::Borrowed“. That is deliberately a syntactic test rather than a semantic one, and it is worth noticing why: a check you can do in one cheap scan is a check you will actually perform. “Return borrowed if nothing changed” would require doing all the work first and then comparing, which costs more than it saves.

Real APIs make exactly this trade. str::trim returns a sub-slice, so it is free. percent_encoding::utf8_percent_encode returns a Cow and borrows when nothing needed encoding. Both pick a cheap conservative test over a perfect one.

For the substitution itself, find('{') and slicing gets you there without building an intermediate Vec<char>. Remember that find returns a byte index, and that slicing a &str at a non-boundary panics — but { and } are ASCII, so the boundaries are safe here.

Lints to expect

  • owned_cow (default-on) fires on a field declared Cow<'static, String>: “needlessly owned Cow type”. The inner type must be the borrowed form — str, not String.
  • ptr_arg (default-on) fires on &Cow<'_, str> parameters: “using a reference to Cow is not recommended”. Take the Cow by value, or take a &str.
  • suspicious_to_owned (default-on) catches cow.to_owned(), which clones the wrapper rather than the contents. You meant into_owned().

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…