RustのDropの実装に対する制約とDrop Checkの規則
DropとDrop CheckについてはThe Book: DropとNomicon: Drop Checkを読んでもらうとして、この辺りの規則はコンパイラの typeck::check::dropck
のコメントに解説がある。
Dropの非伝搬性
ところで、筆者が個人的に勘違いしていた点として、「 T: Drop
である ⇔ T
の破棄時に何かが実行される」という思い込みがあった。実際には例えば、
struct A<T>(T); let x = A(Box::new(true));
とやると、 A<Box<bool>>: Drop
ではないが、 x
の破棄時には Box
のdropが実行される。
Drop
は drop
を実装するためのトレイトにすぎず、これらの性質を親に伝搬させるマーカーの役割は果たしていないということになる。
Dropの実装に対する制約
typeck::check::dropck::check_drop_impl
のドキュメンテーションによると、Dropの実装はある特定の要件を満たす必要がある。簡単に言うと、他のトレイトとは異なり、 Drop
は特殊化が禁止されている。型引数によって Drop
が実装されたりされなかったりする、ということが起きないようになっている。
駄目な実装の例を以下に挙げる。
use std::ops::Drop; struct A<X, Y>(X, Y); // impl<X> Drop for A<X, u32> { fn drop(&mut self) {}} // Bad // impl<X> Drop for A<X, X> { fn drop(&mut self) {}} // Bad impl<Y, X> Drop for A<X, Y> { fn drop(&mut self) {}} // Good struct B<X: ?Sized>(Box<X>); // impl<X: ?Sized + Clone> Drop for B<X> { fn drop(&mut self) {}} // Bad // impl<X> Drop for B<X> { fn drop(&mut self) {}} // Bad impl<X: ?Sized> Drop for B<X> { fn drop(&mut self) {}} // Good
Drop Checkerの規則
Drop Checkerの規則はSound Generic Dropと呼ばれるが、これには2つのバージョンがある。
RFC0769は、それ以前に存在していた #[unsafe_destructor]
というescape hatchを不要にする目的で提案された。これはRustの型にparametricityという仮定(簡単にいうと、型引数による場合分けは発生しないという仮定)をおくことで、より積極的なDrop安全性判定を行うというものであった。
RFC1238は主に、Rust RFC 1210: Impl specialization のように、parametricityの仮定を崩すような拡張を入れるために、RFC0769の変更点の一部を差し戻す方向に再変更するものである。これによりDrop安全性判定はより保守的になったため、新たなescape hatchとして #[unsafe_destructor_blind_to_params]
が追加された。
現在のRFC1238にもとづく規則では、Drop Checkerは以下の規則を検証する。
v
を (一時的な値か名前のついた値かに関係なく) なんらかの値とし、'a
を生存期間 (スコープ) とする。 もし、v
がある型D
のデータを所有していて、しかも
D
が生存期間引数か型引数でパラメーター化されたDrop
実装 (であって、もちろんunsafe_destructor_blind_to_params
でチェックの迂回が指示されていないもの) を持っていて、かつD
の構造から型&'a _
の参照へ到達できるとき、
'a
はv
よりも真に長く生存しなければならない。
(check_safety_of_destructor_if_necessary
のdoc-commentから翻訳した)
それ以前のオリジナルのSound Generic Dropでは、前提が1つ多いためルールが緩い。上と対比する形で書くと次のようになる。
v
を (一時的な値か名前のついた値かに関係なく) なんらかの値とし、'a
を生存期間 (スコープ) とする。 もし、v
がある型D
のデータを所有していて、しかも
D
が生存期間引数か型引数でパラメーター化されたDrop
実装を持っていて、かつD
の構造から型&'a _
の参照へ到達できて、さらに- 次のどちらかが満たされる:
- (A.)
D
のDrop
実装は'a
を直接具体化している (つまりD<'a>
) か、- (B.)
D
のDrop
実装は 1つ以上のメソッドを持つトレイトT
に制約された型引数を1つ以上持っているとき、
'a
はv
よりも真に長く生存しなければならない。
(1つ前の翻訳に追記する形で、Rust RFC 0769から翻訳した。)
Drop Checkerの型トラバース
Drop Checkerは型のメンバに対して再帰的に所有関係を検査する。この規則は以下のようになっている。
Box
, 配列、スライスはその要素を所有する。 (要素数0の配列でも!)PhantomData<T>
は例外的に、T
を所有しているものとみなす。Box
とPhantomData
以外の構造体と列挙体は、とりえる全てのメンバを所有する。- ポインタ、参照は参照先を所有しない。
- 型パラメーターは、何も所有していない。(これは関数の型パラメーターから由来するもので、上で問題になっているparametricityとは別の箇所である。)
まとめ
Dropの挙動は複雑なうえに仕様の変動がある。また、Dropチェッカーは構造体の隠されたフィールドにも影響を受けるので、leaky abstractionの心配がある。