RustのDropの実装に対する制約とDrop Checkの規則

DropとDrop CheckについてはThe Book: DropNomicon: 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が実行される。

Dropdrop を実装するためのトレイトにすぎず、これらの性質を親に伝搬させるマーカーの役割は果たしていないということになる。

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 のデータを所有していて、しかも

  1. D が生存期間引数か型引数でパラメーター化された Drop 実装 (であって、もちろん unsafe_destructor_blind_to_params でチェックの迂回が指示されていないもの) を持っていて、かつ
  2. D の構造から型 &'a _ の参照へ到達できる

とき、 'av よりも真に長く生存しなければならない。

(check_safety_of_destructor_if_necessary のdoc-commentから翻訳した)

それ以前のオリジナルのSound Generic Dropでは、前提が1つ多いためルールが緩い。上と対比する形で書くと次のようになる。

v を (一時的な値か名前のついた値かに関係なく) なんらかの値とし、 'a を生存期間 (スコープ) とする。 もし、 v がある型 D のデータを所有していて、しかも

  1. D が生存期間引数か型引数でパラメーター化された Drop 実装を持っていて、かつ
  2. D の構造から型 &'a _ の参照へ到達できて、さらに
  3. 次のどちらかが満たされる:
    • (A.) DDrop 実装は 'a を直接具体化している (つまり D<'a>) か、
    • (B.) DDrop 実装は 1つ以上のメソッドを持つトレイト T に制約された型引数を1つ以上持っている

とき、 'av よりも真に長く生存しなければならない。

(1つ前の翻訳に追記する形で、Rust RFC 0769から翻訳した。)

Drop Checkerの型トラバース

Drop Checkerは型のメンバに対して再帰的に所有関係を検査する。この規則は以下のようになっている。

  • Box, 配列、スライスはその要素を所有する。 (要素数0の配列でも!)
  • PhantomData<T>は例外的に、 T を所有しているものとみなす。
  • BoxPhantomData 以外の構造体と列挙体は、とりえる全てのメンバを所有する。
  • ポインタ、参照は参照先を所有しない。
  • 型パラメーターは、何も所有していない。(これは関数の型パラメーターから由来するもので、上で問題になっているparametricityとは別の箇所である。)

まとめ

Dropの挙動は複雑なうえに仕様の変動がある。また、Dropチェッカーは構造体の隠されたフィールドにも影響を受けるので、leaky abstractionの心配がある。