プログラミング言語Rustにおいては、値は常にムーブ(memcpyしてメモリ空間上の新しい位置に移動)できるという前提がある。ところでこれは自己参照構造体を扱う上で困るので、Pinという仕組みが導入されており、これを使うと自己参照構造体を上手く扱うことができることが知られている。では、このPinという仕組みはどう実現されていて、またなぜ現在の実装のような形になる必要があったのか?という疑問を設定した。
調べる前に念頭にあったのは、原将己さん(qnighyさん)の「?Sizedが十分ややこしいのでもう金輪際あれを増やしたくないという気持ちがあるらしく、結果?Moveは誕生せず、Pinが生まれた」というツイート。このツイートには納得しかけたが、しかしながら、?Sizedというのは「
コンパイル時に型のサイズが分かっている保証のない型」というだけの意味であり、それを踏襲すれば?Moveというのは「ムーブできる保証のない型」というだけの比較的シンプルな意味論であるだろう。一方で今採用されているPinという仕組みはそれに輪を掛けてややこしい仕組みであり、?MoveではなくPinという解決策が提示されたきっかけが「ややこしさ」のみであるという主張には強く違和感を覚えた。
ちょうどDiscordでhikaliumさんが主催していた「Writing an OS in Rust輪読会」が一段落つき、かつPinについての解説が間に合わずに
終結したので、次週の会合までPinについて調べ、まとめて発表することにした。
まず、
C++では「ムーブできない型」というのを簡単に作ることができる。コピーコンスト
ラクタとムーブコンスト
ラクタを潰せばよい。しかしながら、Rustではコピーコンスト
ラクタを明示的に書く方法は用意されているが、ムーブコンスト
ラクタは書く手段がない。「Rustはあらゆるムーブがmemcpyである」という記載はちらほら確認できるが、そのような保証をどこでしているのかを探すのに手間取った。これについて調べるために、?Moveに関する過去の議論を遡ったところ、「Rustの公式サイトのトップのQ&Aに堂々と『Rustにはムーブコンスト
ラクタはない』と保証している」という旨の発言が見つかったが、リンク切れであった。これはRustの公式サイトが一回大きなリニューアルをしていたからであり、旧い内容はprev.rust-lang.orgというURLに移設されていたからである。消さずに残してくれているのはありがたいのだが、リンクが切れないようにしてくれていたらもっとありがたかったのになぁ……とも思った。
旧サイトが保証していた内容は、「どんな型の値であってもmemcpyによりムーブされる。これにより、代入・関数渡し・関数からのreturnなどで副作用がないことが保証され、多相でunsafeなコードを書くのがたやすくなる」という旨であった。なるほど、
C++のムーブコンスト
ラクタは例外を投げることができるのか。ということでそれを調べてみたところ、
C++ではN2983においてムーブコンスト
ラクタが例外を投げるのを可能としていたことを知った。これは、「例外を投げる可能性がないわけではないが、その可能性が非常に低い」というようなムーブコンスト
ラクタを書けないというのは最適化の機会を妨げることになるからだ、としていた。その代わり、std::move_if_noexceptを用いることで、例外を投げないムーブコンスト
ラクタだけを呼び出すことができるようになった。
さて、前述の通り、Rustにおいてはムーブというのは代入したり関数に渡したり関数から値をreturnしたりするときに気軽に行われる操作である。?Moveに関する過去の議論を遡ったところ、?Moveの明確な欠点はここにあると論じられていた。ムーブができないということはコンスト
ラクタから値をreturnすることができないわけで、つまりは使いたい場所で構造体
リテラルとして記述するしか方法がないということである。これではprivateメンバがある際にどうしようもない。ということで、議論は「借用されるまではムーブを許すというのはどうだ」といった複雑化の方向を辿っていったようである。
最終的に?Moveが採用されなかったのは、?Moveが互換性を破壊するという点が原因であった。今まで型変数が暗黙に持っていた前提を破壊するオプションを付けると言うことは、全ての型引数に対して「ここって ?Move 要るかな?」と考える必要を発生させるということである。他にどうしても方法がないならそれを行うしかないのだろうが、Pinという仕組みが発明されたことにより、
コンパイラに変更を加えることなく自己参照構造体などの「ムーブするとマズい値」を安全に扱う方法が見つかったので、それを採用し?Moveを採用しないということになったようだ。
さて、?Moveが言語に入らなかったということは、Rustにおいては値は常にムーブできるという前提があるということである。ではPinというのは何かというと、「ある値への参照 Pin<’a, P> (ただし P は参照型であるとする)が誕生したら、それ以降は(その値に対する
dropが呼び出されるまでは)参照されている値が動くようなことがあってはなりません」という契約にすぎない。契約を破りうる手段が全てunsafeな
API(呼び出す際に、
プログラマ側が注意して「私は守るべき条件を破っていない」と明示的に確認する必要のある
API)として提供されていることによって、動かしてはいけないと契約した値はunsafeな
APIを用いない限り間違ってムーブしてしまう恐れはない、というカ
ラクリであった。
ところで、このPinの契約には一つ例外規定がある。それは、Unpinという性質を満たしている型の値については契約が成立しないと定められていることである。ほとんど全ての型はデフォルトでUnpinという性質を満たしており、Unpinでない型を作るにはstd::marker::PhantomPinnedという型を構造体のメンバに含める必要がある。この点についてあまり深く言及せずに以上の内容を「Writing an OS in Rust輪読会」で説明したところ、hikaliumさんから「なぜそのような仕組みになっているのでしょうか?そんなUnpinなど用意しなくとも、契約は常に成り立つと規定してもPinという仕組みは成立するはずなのに、なぜ例外規定があるのでしょう?答えをご存知でしたら教えてください、そうでなければ一緒に探しに行きましょう」というご質問を頂いた。確証が持てなかったので、輪読会の残りの時間でそれらの疑問を解消しにいくこととなった。これについては、Pinを導入したRFC2349で「ムーブ不可というのはFutureを扱う際などに必要になるわけだが、ムーブ不可であるFutureもほしい一方で、同時に(自己参照などの問題がない値を扱うための)ムーブ可能なFutureもほしい。二種類のFutureなどを作ることでこれを解決するのも可能ではあるが、エルゴノミックではない」と記載されていた。ここからはきちんと追いかけていないので推測となるが、どうやらPin以前のFutureは参照型に可変参照&mut T を用いていて、それをムーブしないように
プログラマが努力するという方針で実装されていたようだ。ということで、「ムーブしても問題ない型に対してはUnpinが実装されているので&mut Tと同等の機能を提供し、ムーブしたらマズい型というのはPhantomPinnedでその旨明記する」という折衷案になったのでは、という仮説を立てた。