じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

`Cow<'_, T>` で「変更が必要なときだけ allocate する」

はじめに

入力をそのまま返すか、一部書き換えてから返すか、関数の中で判定する場面があります。素直に書くと「変更しない場合も無駄に String::clone() してしまう」、または「&str を返したいが、変更があった時は所有データを返したい」というジレンマに陥ります。

std::borrow::Cow<'_, T> (Clone-On-Write) はこのジレンマを解きます。借用 (Borrowed) と所有 (Owned) のどちらかを選んで返せる enum で、「必要になるまで allocate しない」が型で表現できます。

doc.rust-lang.org

まず動くコード

「すでに全部大文字ならそのまま借用を返し、小文字を含む場合だけ大文字化した所有データを返す」関数。

use std::borrow::Cow;

fn ensure_uppercase(input: &str) -> Cow<'_, str> {
    if input.chars().all(|c| !c.is_lowercase()) {
        Cow::Borrowed(input)              // 変更不要 → 借用のまま返す
    } else {
        Cow::Owned(input.to_uppercase())  // 変更必要 → 所有データを作って返す
    }
}

fn main() {
    let a = ensure_uppercase("HELLO");
    let b = ensure_uppercase("hello");

    println!("a = {a}, borrowed? {}", matches!(a, Cow::Borrowed(_)));
    println!("b = {b}, borrowed? {}", matches!(b, Cow::Borrowed(_)));
}

実行結果は次のようになります。

a = HELLO, borrowed? true
b = HELLO, borrowed? false

a は元の "HELLO" をそのまま借りています (allocate なし)。b"hello" を大文字化した新しい String を持っています (allocate あり)。同じ "HELLO" という文字列でも、片方は借用、片方は所有。Cow がこの違いを型ひとつで表現できるのがポイントです。

Cow の中身

Cow の定義はこうです (簡略化)。

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

B は借用形態 (str, [T], ...) 、<B as ToOwned>::Owned がそれを所有する型 (String, Vec<T>) です。Cow<'a, str> なら Borrowed(&'a str) または Owned(String)

利用者は CowDeref 経由で透過的に扱えます。&str が必要なメソッドはそのまま呼べます。

let s = ensure_uppercase("Hello");
println!("len = {}", s.len());        // どちらの variant でも動く
println!("upper = {}", s.to_uppercase()); // どちらでも動く

「allocate されているかどうか」を呼び出し側が気にする必要はありません。

to_mut で「必要になったら所有に切り替える」

Cow::to_mut(&mut self) を呼ぶと、Borrowed 状態だった場合だけ所有データに切り替えて、&mut Owned を返します。

use std::borrow::Cow;

fn append_exclamation(input: &str) -> Cow<'_, str> {
    if input.ends_with('!') {
        Cow::Borrowed(input)
    } else {
        let mut owned = Cow::Owned(input.to_owned());
        owned.to_mut().push('!');
        owned
    }
}

fn main() {
    println!("{}", append_exclamation("hi"));   // hi!
    println!("{}", append_exclamation("yo!"));  // yo!
}

これは「すでに所有しているなら borrow から脱出して直接書き換える」のではなく、「Borrowed の場合だけ所有版へ変換し書き換え可能にする」という挙動です。

関数の戻り値型として使う

Cow の真価は 戻り値型 で出ます。「入力をそのまま返したい場合もあれば、変更を加えた所有版を返したい場合もある」関数。

use std::borrow::Cow;

fn resolve_greeting<'a>(default: &'a str, override_with: Option<&str>) -> Cow<'a, str> {
    override_with.map_or(Cow::Borrowed(default), |s| Cow::Owned(s.to_owned()))
}

fn main() {
    let g1 = resolve_greeting("hi", None);
    let g2 = resolve_greeting("hi", Some("yo"));

    println!("g1 = {g1}, borrowed? {}", matches!(g1, Cow::Borrowed(_)));
    println!("g2 = {g2}, borrowed? {}", matches!(g2, Cow::Borrowed(_)));
}

設定に上書きが指定されていなければ default をそのまま借用、指定されていれば所有データを作る。「上書きされなかった99%のケースで allocate が起きない」 設計が可能になります。

いつ Cow を使うか

入力データの大半をそのまま使いたいが、まれに変更が必要 な場面で効きます。具体例を挙げます。

  • HTML エスケープ: 危険文字を含まなければ借用のまま、含めばエスケープ後の所有データを返す
  • パスの正規化: すでに正規化されているならそのまま、必要なら正規化後を返す
  • 設定の上書き: 上書きされていなければ default を借用、上書きされたら所有
  • テンプレート展開: プレースホルダがなければ借用、あれば展開後の所有データを返す

逆に「常に変更する」ような関数では、最初から String を返すほうが素直です。Cow「変更しないことが多数派」 な場面で意味があります。

Cow<'_, [T]> も同じ仕組み

Cow は文字列以外でも使えます。Cow<'_, [T]> で「Vec の借用 / 所有」を切り替えられます。

use std::borrow::Cow;

fn ensure_sorted(input: &[i32]) -> Cow<'_, [i32]> {
    if input.windows(2).all(|w| w[0] <= w[1]) {
        Cow::Borrowed(input)
    } else {
        let mut owned = input.to_owned();
        owned.sort_unstable();
        Cow::Owned(owned)
    }
}

fn main() {
    let a = ensure_sorted(&[1, 2, 3]);
    let b = ensure_sorted(&[3, 1, 2]);
    println!("{a:?}, borrowed? {}", matches!(a, Cow::Borrowed(_)));
    println!("{b:?}, borrowed? {}", matches!(b, Cow::Borrowed(_)));
}

すでに昇順なら借用のまま、そうでなければソートして所有版を返す。

まとめ

  • Cow<'_, T>Borrowed(&T)Owned(<T as ToOwned>::Owned) の enum
  • 「変更が必要なときだけ allocate する」を型で表現できる
  • 戻り値型として使うと「変更しないことが多数派」な関数で allocate を回避できる
  • to_mut() で必要になったタイミングで所有版に切り替えられる
  • &str / &[T] どちらでも同じ仕組み

Cow を使うべきか迷ったら、 「この関数の戻り値、入力をそのまま返したいケースが半分以上あるだろうか」を考えてみてください。Yes なら Cow の出番です。

関連