2016年3月9日水曜日

C++プログラマー向けRust 翻訳シリーズ9

Destructuring(造語:非構造化)パート2 matchと無所有権参照


分解を行う際、無所有権参照が関わってくると、驚くことがあるだろう。無所有権参照について深く理解していれば驚くことはないと思うが、議論する価値はある(理解するには結構時間がかかる。間違いないよ。しかも、思ったよりも長いよ。だって、この記事の初稿はめちゃくちゃにしてしまったからね)。

&Enum型の変数xがあるところを想像してみよう(ここで、Enumは何らかのenum型だ)。
選択肢は二つある。*xにマッチをかけて全状態(Variant1 => ...など)を列挙するか、xにマッチさせて全状態への参照(&Variant1 => ...など)を列挙するかのどちらかだ。(流儀次第だが、一番目のやり方のほうが、見た目がすっきりして好ましいだろう)。
変数xは無所有権参照となり、これを被参照する方法には厳密なルールがある。そして、match式と絡むと(少なくとも筆者にとっては)驚異に変わるのだ。特に、既存のenumを一見問題なさそうな方法で変更していると、コンパイラがmatch式のどこかで爆発するのだ。

match式の真髄に触れる前に、Rustの値渡しに関するルールをおさらいしておこう。
C++では、値を変数に代入したり、関数に値を渡す方法は二通りある。値渡しと参照渡しである。
前者が、規定の方法であり、値はコピーコンストラクタ経由かビット単位でコピーされる。
引数や代入先を&記号で修飾すると、値は参照渡しされる。値のポインタだけがコピーされ、これに処理を施すと、コピー前の値にも影響を及ぼす。

Rustでも参照渡しができるが、渡し先と渡し元両方を&記号で修飾しなければならない。
Rustで値渡しをすると、さらにもう二つ選択肢ができる。コピー機構かムーブ機構かの選択だ。コピー機構ならば、C++の値渡しと同じになる(尤も、Rustにコピーコンストラクタはないが)。ムーブを行うと、値をコピーした上で、古い値を破棄する。Rustの型システムにより、古い値にはそれ以上アクセスできないことが保証される。具体的に、int型はコピー機構、Box<int>はムーブ機構である。

fn foo() {
    let x = 7i;
    let y = x;               // xはコピーされる
    println!("x: {}", x);    // OK

    let x = box 7i;
    let y = x;               // xはムーブされる
    //println!("x: {}", x);  // エラー: ムーブ済み(x)の値を使用しようとしている
}
Rustでは、オブジェクトがコピー機構かムーブ機構かは、デストラクタの有無で決定される。デストラクタはおそらく1投稿必要な話題だが、ひとまずDrop traitを実装していればデストラクタを持っていると言えるとだけ説明しておこう。
C++と全く同じように、デストラクタはオブジェクトが破棄される直前に実行される。そして、デストラクタがあるオブジェクトはムーブ機構になる。もしデストラクタがないなら、全フィールドのどれか一つでもデストラクタが定義されていれば、オブジェクト全体がムーブ機構に適応する。そうして、オブジェクトツリーをたどっていき、どこにもデストラクタが見当たらなければ、そのオブジェクトはコピー機構に適応していると判断される。

さて、無所有権参照オブジェクトがムーブされていないことは重要である。その前提がないと、もはや妥当ではない古いオブジェクトへの参照を保有することになり、これは、スコープ外に抜けて破棄されたオブジェクトへの参照を保持しているのと等しい。一種のdanglingポインタである。
ポインタを保持していると、他にも同じオブジェクトを指す参照があるかもしれない。ゆえにオブジェクトがムーブ機構に適応している場合、ポインタを被参照するのは非安全である(一方、コピー機構に適応しているなら、被参照してもコピーを作るだけで古いオブジェクトはそのまま残り、他の参照も生き残り続ける)。

よし、じゃあmatch式に戻ろう。前述したように、&T型の変数xにマッチさせるには、match式内で1回だけ被参照するか、全項で参照にマッチさせるかの2通りの方法がある。

enum Enum1 {
    Var1,
    Var2,
    Var3
}

fn foo(x: &Enum1) {
    match *x {   //選択肢1: ここで被参照
        Var1 => {}
        Var2 => {}
        Var3 => {}
    }

    match x {
        //選択肢2: 各項で被参照
        &Var1 => {}
        &Var2 => {}
        &Var3 => {}
    }
}
今回の場合、Enum1はコピー機構に適応しているので、どちらの手段も選択できる。順につぶさに見ていこう。
一つ目の選択肢では、変数xEnum1型の一時変数(変数xの値をコピーしている)に被参照し、Enum1型の各状態に対してマッチを行う。この方法は、値の型までは見ないから1段階マッチングになる。
一方、二番目の方法では、被参照は行わない。&Enum1型の値を各状態の参照とマッチさせることになる。こちらのマッチングは2段階である。まず、型(必ず参照)に対してマッチさせ、さらに参照化された型(Enum1)にマッチさせるのだ。

どちらにせよ、プログラマー(コンパイラ)がムーブや参照に関する不変性を保っていると確認せねばならない。参照されているオブジェクトは、一部であってもムーブしてはならないのだ。
もし対象の値がコピー機構に適応しているなら、これは大したことない。
しかし、ムーブ機構に適応している場合、全項でムーブが発生していないことを確認せねばならない。これを実現するには、ムーブが発生するデータを無視するか、参照を作ればいい(こうすれば、ムーブ渡しではなく参照渡しになる)。

enum Enum2 {
    // Boxはデストラクタ持ちなので、Enum2はムーブ機構に適応している
    Var1(Box),
    Var2,
    Var3
}

fn foo(x: &Enum2) {
    match *x {
        // 内包されたデータを無視するので、大丈夫
        Var1(..) => {}
        // 他項には変更なし
        Var2 => {}
        Var3 => {}
    }

    match x {
        // 内包されたデータを無視するので、大丈夫
        &Var1(..) => {}
        // 他項には変更なし
        &Var2 => {}
        &Var3 => {}
    }
}
いずれのアプローチでも、内包データを参照していないので、ムーブされることはない。
一番目の手段で、変数xは参照されているものの、被参照のスコープ(つまり、match式全体)内で中身には触れていないから、何も逃すことはない。また、値自体を束縛(*xを変数に束縛するということ)してもいないので、このオブジェクトをムーブしてもいない。

二番目のアプローチで各項において参照を取ることができるが、被参照を行う一番目では無理だ。ゆえに、二例目の2番目の項をa @ &Var2 => {}と書き換える(ここで変数aは参照)ことはできるが、一例目でa @ Var2 => {}と書いてしまうと、*xを変数aにムーブすることになってしまうため不可能だ。
ref a @ Var2 => {}となら書き換えられる(ここで変数aもまた参照)が、あまり見かける記法ではない。

では、Var1に内包されたデータが必要ならばどうすればいいだろうか。以下のような書き方はできない。

match *x {
    Var1(y) => {}
    _ => {}
}
または
match x {
    &Var1(y) => {}
    _ => {}
}
理由は、どちらの場合も、変数xの一部を変数yにムーブすることになってしまうからだ。
refキーワードを使用して、Var1内のデータへの参照を得ればいい。つまり、&Var1(ref y) => {}と書く。これならば、どこにも被参照はなく、変数xの一部をムーブすることにはならないからだ。その代わり、変数xの中身を指すポインタを作成することになる。

別の解決策として、Boxオブジェクトに分解することもできる(こうすると3段階マッチングになる)。つまり、&Var1(box y) => {}と書く。こうすると、int型はコピー機構に順応しており、変数yは、Var1内のBoxオブジェクトに包まれたint値のコピーになるからだ(この時、Var1はさらに無所有権参照になっている)。int型はコピー機構のため、変数xの一部をムーブする必要はない。
また、int値をコピーするのではなく、参照とすることもできる。つまり、&Var1(box ref y) => {}と書くのだ。この場合、どこにも被参照はなくなり、変数xの一部をムーブする必要がなくなるのだ。
仮にBoxオブジェクトの中身がムーブ機構に適応している場合、&Var1(box y) => {} と書くことはできず、参照バージョンを使わざるをえない。
さらにさらに、以前挙げた例の一番目においても、同様のテクニックを駆使することができ、いずれも先頭の&記号がなくなるだけの違いにしかならない。例えば、Var1(box ref y) => {}などね。

さあ、さらに複雑度を上げていこう。
1組のenum値に対してマッチングさせたいとする。こうなると、もう一番目のアプローチを取ることはできない。

fn bar(x: &Enum2, y: &Enum2) {
    // エラー: 変数xとyはムーブされてしまう
    // match (*x, *y) {
    //     (Var2, _) => {}
    //     _ => {}
    // }

    // OK
    match (x, y) {
        (&Var2, _) => {}
        _ => {}
    }
}
最初のアプローチは不正になる。理由は、マッチ対象の値が変数xyを被参照することで作成され、新しいタプルオブジェクトにムーブされているからだ。そのため、今回は二番目のアプローチしかうまく動作しない。その上、言うまでもないことだが、上述したルールに従って変数xyの一部をムーブしないようにしなければならない。

もし、データに対して参照しか得られないにもかかわらず、値が必要な場合は、このデータをコピーする以外に手段はない。大体の場合、それはcloneメソッドを使うことを意味する。データがcloneメソッドを実装していない場合、さらに分解を行って手動でコピーを行うか、自分でcloneメソッドを実装することになる。

では、ムーブ機構を持った値への参照ではなく、値自体が存在する場合はどうだろうか。
今度は、ムーブしても大丈夫になる。なぜなら、他にこの値への参照がないことが明白になるからだ(もし、参照が存在していたら、コンパイラによって値の使用を制限されてしまう)。

fn bad(x: Enum2) {
    match x {
        Var1(y) => {}
        _ => {}
    }
}
ここでも、注意事項がある。
まず、ムーブは1カ所でしか行えない。上記の例で、変数xyの一部をムーブし、他は忘却の彼方へ追いやっている。しかし、仮にa @ Var1(y) => {}と書いて、変数xの全体を変数aに、変数xの一部を変数yにムーブしようとすると失敗する。このような項は不正なのだ。
しかも、変数ayを参照に変えても無駄だ。こうすると今度は、以前解説した参照中ムーブ問題に直面してしまう。
変数ay両方を参照に変えるのはありだ。これなら、いずれもムーブされず、変数xはそのまま残り、その全体と一部へのポインタを得られる。

同様に(かつ、より一般的に)、複数のデータを内包する状態がある場合、あるデータには参照をとって、他はムーブするという芸当は行えない。
具体的に言うと、Var4(Box<int>, Box<int>)と定義されたVar4があるとして、match項内でVar4(ref y, ref z) => {} という風に両方を参照したり、Var4(y, z) => {}という風に両者をムーブしたりすることはできるが、Var4(ref y, z) => {}という風に一方はムーブして、もう片方は参照するといったことはできないということである。
なぜなら、オブジェクトは一部でもムーブしたら全体が破棄されてしまい、参照が無効になってしまうからだ。


原文: https://github.com/nrc/r4cppp/blob/master/destructuring%202.md

0 件のコメント:

コメントを投稿

なにか意見や感想、質問などがあれば、ご自由にお書きください。