📋

Rust: Copyトレイトが実装された型を持つ値を無理やりムーブする方法となぜ必要か

2022/09/19に公開約3,400字2件のコメント

以下の記事のサイド記事です。

https://zenn.dev/luma/articles/rust-auto-call-stack-in-heap

Copyトレイト

Types whose values can be duplicated simply by copying bits.
source: https://doc.rust-lang.org/std/marker/trait.Copy.html

意訳: ビットをコピーすれば複製できる型だよ

ヒント: 「ビットのコピー」と「複製」が意味するものの違いって?

「ビットのコピー」は、そのままメモリ上のビット配列をコピーすることです。
「複製」というのは、Rustレベルでの値としてみた時に、所有物として複製されているか、ということです。

例えば、 Vec は Copy が実装されていません。 Vec は当然 Sized なので、それ単体の「ビットのコピー」は常に定数時間ですぐに終わります。
しかし、Vec の中身は、実際の要素がどこにあるか、というポインタ(スライス)が含まれており、その先にある実際のデータまで複製しないと、Rustレベルでの値としての複製は完了しません。

Copyトレイトはムーブが起こらない

Copyトレイトが実装されていると、見かけ上ムーブするような見た目にしても、所有権がムーブした扱いにはなりません。

fn main () {
  // u32 は Copy トレイトが実装されている
  let a: u32 = 0;
  let b = a;
  let c = a; // Copyトレイトがなければ、ここでムーブ後のムーブが起きている(つまりエラー)はず
}
fn main () {
  // Vec<T> は Copy トレイトが実装されていない
  let a: Vec<u32> = vec![1, 2, 3];
  let b = a;
  let c = a; // エラー: use of moved value
}

ムーブしたいとき

本題です。まずは、状況を設定しておきます。これは実際に冒頭で紹介した記事で起きた状況を簡略化したものです。

1. 以下のように、クロージャ(内側)を返す関数(外側)を考えます。この時点では、外側は変数をキャプチャしていないので、クロージャではありません。

fn main() {
    let make_adder = |x: u32| |y: u32| x + y;
    let adder3 = make_adder(3);
    println!("{}", adder3(8));
}

2. 上記は動きません。x + yxu32、つまり Copy であるが故に、ムーブが起こらず、借用のみをしています。しかし、内側のクロージャを返し終わったら x はdropされてしまうので、借用ではなくムーブする必要があります。

fn main() {
    let make_adder = |x: u32| move |y: u32| x + y;
    let adder3 = make_adder(3);
    println!("{}", adder3(8));
}

3. ここで、さらに外側から別の変数を借用したい状況を考えます。

fn main() {
    let bias = vec![1, 2, 3];
    let make_adder = |x: u32| move |y: u32| bias[0] + x + y;
    let adder3 = make_adder(3);
    &bias; // 何かしらで使う
    println!("{}", adder3(8));
}

4. 上記もエラーです。biasは借用しかしていない見た目になっていますが、 move を指定しているせいで、ムーブしてしまいます。

さて、ここで、 x はムーブしつつ、bias は借用したいです。少し情報を探すと、 move を指定せずに、ムーブしたいものは事前に再代入することでムーブできるとあります。
これは、例えば xCopy でなければうまく行く方法です。

fn main() {
    let bias = vec![1, 2, 3];
    let make_adder = |x: Vec<u32>| |y: u32| {
      let x = x; // x をムーブしておく。
      bias[0] + x + y // ここでは bias も x も借用しかされていない。
    }
    let adder3 = make_adder(vec![3]);
    &bias; // 何かしらで使う
    println!("{}", adder3(8));
}

5. しかし、以下のように、Copyが実装されていると同様にうまくいきません。

fn main() {
    let bias = vec![1, 2, 3];
    let make_adder = |x: u32| {
        |y: u32| {
            let x = x; // ムーブのような見た目でも、Copyなので借用しか起こらない。
            bias[0] + x + y // ここでは bias も x も借用しかされていない。
        }
    };
    let adder3 = make_adder(3);
    &bias; // 何かしらで使う
    println!("{}", adder3(8));
}

長くなりましたが、以上が問題設定です。次のステップで解決になります。なお、実際の元の記事の状況は、内側はクロージャではなくasyncブロックでしたが、わかりやすさのためにクロージャにしました。 (ただ、FnOnceになってしまうという実用性の乏しさは逆にありそうですが…)

強制的にムーブセマンティクスを利用する方法

強制的に、と言いつつ Copy はどう頑張ってもムーブは起こらないので、単に Copy トレイトを持たない構造体に一旦移し替えるだけです。
ForceMover という構造体を作って移し替えを行っています。

struct ForceMover<T>(T);

fn main() {
    let bias = vec![1, 2, 3];
    let make_adder = |x: u32| {
        let x = ForceMover(x);
        |y: u32| {
            let x = x; // まずムーブする。
            let x = x.0; // これだけで良さそうだが、こちらは同様にCopyトレイトなので、ムーブの見た目で借用しか起こらない。
            bias[0] + x + y
        }
    };
    let adder3 = make_adder(3);
    &bias; // 何かしらで使う
    println!("{}", adder3(8));
}

なお、#[derive(Clone, Copy)] をつけると、またコピーが起きてしまいます。


以上です。どちらかというと、なぜCopyトレイトをわざわざムーブしたい状況があるのか、というのに対するアンサーの意味もありました。

GitHubで編集を提案

Discussion

今回の件はMove/Copyの違いではなく、closureのcaptureのしかたの問題です。もっと言うと、C++ のように特定の変数は参照、他は値でキャプチャすると指定できれば解決です。確かにRustにはそのようにexplicitにどの変数を値/参照でキャプチャするかを選択する文法は無いですが、できないわけではありません。次のstackoverflowの回答のようにmoveをする前に事前に参照を他の変数にバインドしておけばいいのです。

https://stackoverflow.com/a/67230904

今回の例ではmoveをする前にbiasだけ事前に参照を取得しておけばいいので以下のように簡単に解決できます。

fn main() {
    let bias = vec![1, 2, 3];
    let make_adder = |x: u32| {
        let bias = &bias;
        move |y: u32| bias[0] + x + y
    };
    let adder3 = make_adder(3);
    dbg!(&bias);
    println!("{}", adder3(8));
}

ありがとうございます!たしかにそうですね…

似たような記事でこの辺は見ていたのですが、たしかに問題としてはexplixitな方法がほしいというところかもしれません…

https://stackoverflow.com/questions/58459643/is-there-a-way-to-have-a-rust-closure-that-moves-only-some-variables-into-it

思い出した点のもう一つの点としては、もうひとつ問題設定が抜けていて、マクロで内側のクロージャだけ受け取って(借用されている変数がわからない状況で…)…というのがありました…。

一般論的なところではない感じはすごいあるのでちょっとタイトルか問題を修正することは考えます。

ログインするとコメントできます