Cow::Owned & custom string types

#1

I’ve been messing with some custom string types - in particular, stack-allocated small strings like InlinableString, smallstr and arrayvec::ArrayString. I’ve noticed that in each case I’ve tried, the following shape of code doesn’t compile:

        let s: SmallString<[u8; 8]> = SmallString::from("foo");
       
        // This is fine
        let borrowed: Cow<str> = Cow::Borrowed(&*s);
        assert_eq!("foo", borrowed);
  
        // This blows up
        let owned: Cow<str> = Cow::Owned(s);
        assert_eq!("foo", owned);

This produces:

error[E0271]: type mismatch resolving `<str as std::borrow::ToOwned>::Owned == string::SmallString<[u8; 8]>`                                                                                   
   --> src/string.rs:970:31                                                                                                                                                                    
    |                                                                                                                                                                                          
970 |         let owned: Cow<str> = Cow::Owned(s);                                                                                                                                             
    |                               ^^^^^^^^^^^^^ expected struct `std::string::String`, found struct `string::SmallString`                                                                    
    |                                                                                                                                                                                          
    = note: expected type `std::string::String`                                                                                                                                                
               found type `string::SmallString<[u8; 8]>`  

I’ve tried a few variants but can’t make it work.

Why is this? All these types implement Borrow<str> and Clone - why isn’t that enough?

Is there a straightforward fix - either implementing some missing trait for SmallString etc, or setting up the Cow::Owned differently?

Absent a solution, when optimizing some string manipulation, it seems I have to chose between getting copy-on-write behavior / avoiding unnecessary allocations, and making allocations on the stack when they’re necessary. Which is unfortunate.

0 Likes

#2

That’s because SmallString only implements Deref with the target being &str, and not str, meaning that you will never get the actual str, and will instead get a &str (meaning that you can’t instantiate a Cow<str> from a type that only implements Deref<Target=str> because deref() returns &Target which disallows you from taking ownership of the underlying Target [Which in this case is str])

1 Like

#3

Intuitively, it’s because Cow is designed only to work with types where the “borrowed” version is tied to exactly one “owned” type. It’s Cow<str>, not Cow<String, str> - there can only one “owned” part for each borrowed.


If you want the type-system reason, first look at the definition of Cow:

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

So to construct Cow::Owned around a SmallString, you need some type B such that <B as ToOwned>::Owned = SmallString. But look now at the definition of ToOwned:

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

We want B::Owned = SmallString, and this trait specifies Owned: Borrow<B>, so we know B must satisfy SmallString: Borrow<B>. If we look at the documentation for SmallString, the only implementation for Borrow is

impl<A: Array<Item = u8>> Borrow<str> for SmallString<A> { ... }

So know we know that if we want to construct Cow::Owned(small_string), we need B = str.

But then going back to the definition of cow, <B as ToOwned>::Owned is <str as ToOwned>::Owned. Owned is an output type here, which means there’s exactly one for str. Looking again at the documentation for this impl, we see that <str as ToOwned>::Owned = String. SmallString != String, so we can’t construct Cow::Owned around a SmallString if B = str. As str was the only candidate for b, and it’s now impossible, the compiler stops.

Hope that helps explain the reasoning?

2 Likes

#4

Thanks! I get the general idea at least.

1 Like