読者です 読者をやめる 読者になる 読者になる

Rustで値のmoveが起こる場合

Rust v.0.11pre時点での情報です。

Rustにおけるムーブセマンティクス

だいたいC++のそれと同じものと理解してるんだけど、おさらいとして。

Rustの言語セマンティクスの一つに、メモリの所有権(Ownership)というものがある。これは「このメモリ領域を自身のもの所有しているものは常に一つ(ユニーク)になる」というもので、これが保証されると、特定のメモリの領域は、所有する変数ひいては所有するタスクが決定されるようになる。原則として、各タスクごとの単位でしか、その所有権の貸し借り(borrowing)ができないため、タスクAから他の非同期に動作するタスクBの所有するメモリ領域の変更は不可能になる。タスクAからタスクBに向けてデータを送信した場合、データの所有権ごと送信されるので、結果としてデータ競合が発生しないことが保証される(もちろんArc<T>とか使ったり、生ポインタを送信するみたいな抜け道はありますよ)。

このOwnershipの概念に基づいて、変数のmutability規則が決まっていたり、borrowingの説明がなされるようになる。タスク間でデータをやりとりできる型かどうかについては、Send kindであるかどうかとかが絡んでくるんだけど、マニュアル読んだ方が速いのでリンクを貼るだけにとどめる。

もうひとつ、Rustではポインタに関しては、基本的にはshallow copyとなる。 なので、チュートリアルに載っている以下のようなコードでは、ポインタの複製にとどまる。

let xs = Cons(1, box Cons(2, box Cons(3, box Nil)));
let ys = xs; // copies `Cons(u32, pointer)` shallowly

ここで問題発生。

ここでコピーされたポインタ値はowning pointerのもの。即ち、そこにはownershipが存在している。だが、ポインタ値のみがコピーされたことで、所有していると見なされるオブジェクトが2つに増えてしまった。これではOwnershipの説明と矛盾する。

Rustでは、この矛盾の解消のために、ownershipを移動するということを行う。さっきのコードの例で言えば、子孫となっているConsオブジェクトの所有権は、ポインタのコピーと同時にxyからysに対して移動(ムーブ)される。そして、古い方の所有者であるxyは無効なものとして、以後、使用することはできなくなる(コンパイルエラー!)。

以上が、RustにおけるOwnershipと、それに基づくムーブセマンティクスの簡単な説明になる。 これらにより、セマンティクスの面では安全性が保証され、処理系の最適化という文脈では余計なメモリコピーが発生しなくなるのでうれしくなる、というわけ。

ムーブが起こる場合

それでは、ムーブが発生する場合は具体的にどういうケースなのかということになるので、リファレンスマニュアルから、一通り抜き出してみた。

Rustにおける代入などの操作で、copyではなくmoveとなる条件は以下の通り:

  • 型がコピーではなくムーブの対象と指定されている場合(代入など)
    • 例としては、型が内部にowning pointer(Box<T>)を含む
    • Drop traitを実装している
      • つまり、Type kinds(型の種類)Dropになる場合。尚、Dropkindは、制約上Send+Dropになる.(ムーブ対象の値を子孫方向に含む場合も同様)
    • Defaultとして、デストラクタ(Drop::drop())を実装している型、クロージャ環境、非ファーストクラスな型はコピー不可でありムーブする
      • vectorが固定長である([int, ..10]のように表現できる)場合は、静的にサイズが決定できる為、ファーストクラスとなる
      • 一方、可変長である場合(Vec<T>または&[T]を用いてのみ生成可能)、非ファーストクラスとなる。
  • returnを用いて値を返す場合
    • 関数でreturnを省略する場合には、最後の式が値を生み出す場合は、暗黙的にreturnで返していると解釈されるので、returnを明示しようがしまいが関数の戻り値はmoveする
    • ブロック式の末尾の値が返る場合も同じ様子
  • ジェネリック関数においては、ジェネリックな型が不明瞭(opaque)である場合、引数の関数内での挙動はmoveに限定される
    • <T: Copy>のように)型がCopy kindであることが指定されていれば、copyされる
  • クロージャ型がプロシージャ(proc)である場合、参照する外部変数はムーブされる
  • タスク間でデータをやり取りする場合
    • この場合、Sendkindである必要がある