std::borrow::cow

自転車本では紹介されてないけれど、Rustならでは必要とされる面白い型、Copy on Writeではなくて、Clone on Writeを意味するstd::borrow::Cow の紹介です。

前提知識:strStringの違い

前提知識:文字列定数とその参照

ローカル変数

まず、以下のように関数内のローカルデータとして確保された文字列定数を参照する変数の型を確認しておきます。

/// コンパイルできる
fn f() {
  let s = "a fixed string";
  ...

これは &str です。

/// コンパイルできる
fn f() {
  let s: &str = "a fixed string";
  ...

ちなみにsがグローバル変数Sになるとどうなるでしょう。

/// 設計中
const S: ??? = "a fixed string";

グローバル変数も他の言語同様にヒープではなくOSで言うところのデータ領域に置かれるのでやはりstrであり、 その領域を指すので変数S&str型になります。

/// コンパイルできる
const S: &str = "a fixed string";

前提知識:strString間の変換

&strからString

&str型の変数があればその指している対象からto_stringメソッドを使ってStringを作ることができます。

/// コンパイルできる
fn f() {
  let s: &str = "A fixed string" 
  let t = s.to_string();     // ヒープ操作を必要とする
  ...

この時tの実体はヒープ上に置かれたfat pointerです。固定長なのでメモリを大きく消費するわけではありませんが、実体へのポインタを含む構造体をヒープ上に構成する必要があります。

Stringから&str

逆の操作はas_str()です。この操作はStringを構成するfat pointerを流用すればいいので極めて軽量です。 これは&srtからStringへの変換がto_*系なのに対し、Stringから&strへの変換がas_*系の命名になっていることからもわかります。

/// コンパイルできる
fn f() {
  let s: String = "A fixed string".to_string();
  let t: &str = s.as_str();    // 軽量な操作
  ...

ここまでが前提知識でした。

問題となるシナリオ: &strString の混在

さて、以下のような構造体Sに対してその文字列表現を返すrep()メソッドを定義するとします。

struct S {
  index: usize,
  vec: Vec<usize>,
}

ただし、

とします。

/// 設計中
impl S {
  fn rep(&self) -> ??? {
     if self.index == 0 {
         ...
     } else {
         ...
     }
  }
}

Case 1: index == 0 のオブジェクトの場合

この場合、rep()内部で固定のメッセージを保持するローカル変数mesの値をそのまま返すことにします。 mesの型は &str であることからrep()の返値型も&strになります。

/// コンパイルできるはず
impl S {
  fn rep(&self) -> &str {
     if self.index == 0 {
         let mes: &str = "null object";
         mes
     } else {
         ...
     }
  }
}

Case 2: それ以外

フィールドvecの値を埋め込んだ文字列を作るためformat!を使うことにしました。

format!の返す型はStringなのでrep()の返値型もStringになります。

/// コンパイルできない
impl S {
  fn rep(&self) -> String {


         ...
     } else {
         format!("S{{{:?}}}", self.vec)
     }
  }
}

ここで型が一致しない問題に直面します。

案1: &strへの統一

既に見たようにどちらの方向にも変換できるのでまず&strへ統一することを考えてみます。

  fn rep(&self) -> &str {
     if self.index == 0 {
         let mes: &str = "null object";
         mes
     } else {
         format!("S{{{:?}}}", self.vec).as_str()
     }
  }
}

これはライフタイム制約を満足しないエラーになります。

error[E0515]: cannot return value referencing temporary value
   |
   |          format!("S{{{:?}}}", self.vec).as_str()                                                        
   |          ------------------------------^^^^^^^^^
   |          |
   |          returns a value referencing data owned by the current function
   |          temporary value created here

以下のようにローカル変数resにバインドしても、

  fn rep(&self) -> &str {
     if self.index == 0 {
         let mes: &str = "null object";
         mes
     } else {
         let res = format!("S{{{:?}}}", self.vec);
         res.as_str()
     }
  }
}

ライフタイムが短すぎることには変わりはないので、エラーになります(resはヒープに置かれても所有者であるrepからexitする時点で回収されてしまう)

error[E0515]: cannot return value referencing local variable `res`
   |
   |          res.as_str()                                                        
   |          ---^^^^^^^^^
   |          |
   |          returns a value referencing data owned by the current function
   |          `res` is borrowed here

なので、format!で作ったString実体を呼び出し側に渡さないといけません。

案2: Stringへの統一

ということで&str型のmesを返しているパスの型を変えることにします。関数の返値型を変えてコンパイルすると以下のようなエラーメッセージが表示されます。

error[E0308]: mismatched types
  --> src/main.rs:13:10
   |
   |   fn rep(&self) -> String {                                                                               
   |                    ------ expected `std::string::String` because of return type
...
   |          mes                                                                                           
   |          ^^^^
   |          |
   |          expected struct `std::string::String`, found `&str`
   |          help: try using a conversion method: `mes.to_string()`

ヘルプに従って修正すれば問題はなくなります。

  /// コンパイルできる
  fn rep(&self) -> String {
     if self.index == 0 {
         let mes: &str = "null object";
         mes.to_string()
     } else {
         let res = format!("S{{{:?}}}", self.vec);
         res.as_str()
     }
  }
}

しかしこれは、必要とは思われないヒープでのオブジェクト生成をしているため、時間的にも空間的にも(できれば避けたい)コストをかけてしまっています。ゼロコストアブストラクションをうたうRustのプログラムとしては是非とも避けたいところです。

案3: 型の包含

この問題を解決するには「借用」と「実体」のどちらも返せるようなenumを用意するという手が使えます。

/// 設計中(ライフタイム指定がまだついていない)
enum WrapStr {
  from_str(&str),
  from_format(String),
}

impl S {
  fn rep(&self) -> WrapStr {
     if self.index == 0 {
         let mes: &str = "null object";
         WrapStr::from_str(mes)
     } else {
         WraStr::from_format(format!("S{{{:?}}}", self.vec))
     }
  }
}

こうすれば型の問題は解決するし、見かけ上構造体で包むコストは(おそらく)コンパイラの最適化中に何もしないコードに変換されることが期待できます。ということでWrapStrを追加定義すれば問題解決します。ポインタを含むのでライフタイム制約が必要かな。。。

Cow

しかし自分で定義するよりも、このような状況のための型がすでに標準ライブラリに用意されているのでそれを使いましょう。 それがClone on Write, Cow型です。これは以下のように定義されています。 https://doc.rust-lang.org/std/borrow/enum.Cow.html

/// https://doc.rust-lang.org/std/borrow/enum.Cow.html
pub enum Cow<'a, B> 
where
    B: 'a + ToOwned + ?Sized, 
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

ToOwnedは借用したデータから、所有権を持つ実体を構成することができるというトレイトです。 文字列関連では以下のようになっています。 https://doc.rust-lang.org/std/borrow/trait.ToOwned.html

/// https://doc.rust-lang.org/std/borrow/trait.ToOwned.html
impl ToOwned for str
  type Owned = String
  
  /// Examples
  let s: &str = "a";
  let ss: String = s.to_owned();

つまりstrからStringが作れると。これを見ながらCowの定義のBstrに変換してやると以下のようになります。

pub enum Cow<'a, str> 
where
    str: 'a + ToOwned + ?Sized,  // 条件OK
 {
    Borrowed(&'a str),
    Owned(String),
}

ということで、借用(&str)はCow::Borrowedで実体(String)はCow::Ownedで包んでやればいいことがわかりました。

最終的なプログラムはこうなります。

use std::borrow::Cow;

impl S {
  /// コンパイルできる
  fn rep(&self) -> Cow<'_, str> {
    if self.index == 0 {
      Cow::Borrowed("Null S")                  // 場所は確保済み => 借用したい
    } else {
      Cow::Owned(format!("S[{:?}]", self.vec)) // 借用ではダメ =>実体を渡したい
    }
  }
}

使う側では一回derefしてやれば借用であったか実体であったかを気にする必要はありません。

   println!("{}", *s.rep());

ちなみにderefしたものが何型になっているかというと、

  // コンパイルできる
  let temp: &str = &*s.rep();

だそうです。文字列のスライスみたいですね。

コストについて

derefのコストは以下に引用したようにポインタ辿りだけなので、「軽量」と言ってしまっていいでしょう。

https://doc.rust-lang.org/src/alloc/borrow.rs.html#320

/// https://doc.rust-lang.org/src/alloc/borrow.rs.html#320
#[stable(feature = "rust1", since = "1.0.0")]
impl<B: ?Sized + ToOwned> Deref for Cow<'_, B> {
    type Target = B;

    fn deref(&self) -> &B {
        match *self {
            Borrowed(borrowed) => borrowed,
            Owned(ref owned) => owned.borrow(),
        }
    }
}

https://doc.rust-lang.org/src/core/borrow.rs.html#212

/// https://doc.rust-lang.org/src/core/borrow.rs.html#212
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Borrow<T> for T {
    fn borrow(&self) -> &T {
        self
    }
}

https://doc.rust-lang.org/src/core/borrow.rs.html#226

/// https://doc.rust-lang.org/src/core/borrow.rs.html#226
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Borrow<T> for &T {
    fn borrow(&self) -> &T {
        &**self
    }
}