Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Drop の詳細化

動的 drop

リファレンスによると、次のとおりです。

初期化済みの変数または一時値がスコープを抜けると、そのデストラクタが 実行される、つまり drop されます。代入も、その左辺オペランドが初期化済みであれば、 そのデストラクタを実行します。変数が部分的に 初期化されている場合、初期化済みのフィールドだけが drop されます。

MIR を構築するとき、Drop および DropAndReplace 終端命令は drop が発生し得る場所を表します。しかし、このフェーズでは、これらの 終端命令が存在しても、デストラクタが実行されることは保証されません。これは、 drop の対象が終端命令に到達する前に未初期化になっている可能性があるためです (通常は、そこからムーブされているためです)。一般に、変数が初期化されているかどうかを コンパイル時に知ることはできません。

#![allow(unused)]
fn main() {
let mut y = vec![];

{
    let x = vec![1, 2, 3];
    if std::process::id() % 2 == 0 {
        y = x; // 条件付きで `x` を `y` にムーブする
    }
} // ここで `x` はスコープを抜ける。drop されるべきか?
}

このような場合、変数が初期化されているかどうかを 動的に追跡する必要があります。ルールの詳細は RFC 320: Non-zeroing dynamic drops に示されています。

Drop 義務

RFC から引用します。

ローカル変数が初期化されると、「drop 義務」の集合、つまり drop される必要がある構造的パス(たとえばローカルの a や、 フィールドへのパス b.f.y)の集合が確立されます。

構造体型 T のローカル変数 x に対する drop 義務は、 T の構造を分析することで計算されます。T 自体が Drop を実装している場合、 x が drop 義務になります。TDrop を実装していない場合、 drop 義務の集合は T のフィールドの drop 義務の和集合になります。

構造的パスからムーブされる(したがって未初期化になる)と、そのパスまたはその子孫 (path.fpath.f.g.h など)に対する drop 義務はすべて解放されます。Drop 実装を持つ型では個々の フィールドからのムーブが許可されないため、それらを通じて初期化状態を追跡する必要はありません。

ローカル変数がスコープを抜ける(Drop)とき、または構造的パスが 代入によって上書きされる(DropAndReplace)とき、その変数またはパスに対する drop 義務があるかどうかを確認します。その時点までに義務が 解放されていない限り、関連付けられた Drop 実装が呼び出されます。 enum 型については、「アクティブな」バリアントに対応するフィールドだけを drop する必要があります。このような型の drop 義務を処理する場合、まず 判別子を確認してアクティブなバリアントを決定します。アクティブなもの以外のバリアントに対する drop 義務はすべて無視されます。

これらのルールを説明するために、いくつか興味深い型を示します。

#![allow(unused)]
fn main() {
struct NoDrop(u8); // `Drop` 実装はない。`Drop` 実装を持つフィールドもない。

struct NeedsDrop(Vec<u8>); // `Drop` 実装はないが、`Drop` 実装を持つフィールドがある。

struct ThinVec(*const u8); // カスタム `Drop` 実装。個々のフィールドからはムーブできない。

impl Drop for ThinVec {
    fn drop(&mut self) { /* ... */ }
}

enum MaybeDrop {
    Yes(NeedsDrop),
    No(NoDrop),
}
}

Drop の詳細化

これらのルールの有効なモデルの 1 つは、関数内のどこかで使用される すべての構造的パスに対して真偽値フラグ(「drop フラグ」)を保持することです。このフラグは、 そのパスが初期化されるとセットされ、そのパスからムーブされるとクリアされます。 Drop が発生すると、Drop の対象に関連付けられたすべての義務について フラグを確認し、まだ適用可能なものについては関連付けられた Drop 実装を呼び出します。

このプロセス、つまり不正確な Drop および DropAndReplace 終端命令を持つ新しく構築された MIR を、drop フラグを持つものへ 変換することは、drop の詳細化として知られています。MIR 文によって変数が初期化済み (または未初期化)になると、drop の詳細化はその変数の drop フラグをセット(またはクリア)するコードを挿入します。これは、新しく挿入された drop フラグを確認する 条件分岐で Drop 終端命令をラップします。

drop の詳細化はまた、DropAndReplace 終端命令を、対象の Drop と、 新しく drop された place への書き込みに分割します。これは、上で説明した内容とはやや無関係です。

これが完了すると、MIR 内の Drop 終端命令は、drop される place の型に対する 「drop glue」または「drop shim」の呼び出しに対応します。ある型の drop glue は、その型の Drop 実装(存在する場合)を呼び出し、その後 その型のすべてのフィールドに対して再帰的に drop glue を呼び出します。

rustc における Drop の詳細化

上で説明したアプローチは、必要以上にコストが高くなります。いくつかの最適化を 考えることができます。

  • Drop の対象である(またはその対象を接頭辞として持つ)パスだけが drop フラグを必要とします。
  • いくつかの変数は、drop される時点で初期化済み(または未初期化)であることが 既知です。これらには drop フラグは不要です。
  • パスの集合が共有された接頭辞を通じてのみ drop されたりムーブされたりする場合、それらの パスは単一の drop フラグを共有できます。

これらの一部は rustc に実装されています。

コンパイラでは、drop の詳細化はいくつかのモジュールに分割されています。パス 自体はここで定義されていますが、主なロジックは 別の場所で定義されています。これは、drop shim の構築にも使用されるためです。

drop の詳細化は、新しく構築された MIR 内の各 Drop を 4 種類のいずれかとして 指定します。

  • Static、対象は常に初期化済みです。
  • Dead、対象は常に初期化です。
  • Conditional、対象は全体として初期化済み、または全体として 未初期化のどちらかです。部分的に初期化されていることはありません。
  • Open、対象は部分的に初期化されている可能性があります。

このために、MaybeInitializedPlacesMaybeUninitializedPlaces という 1 組のデータフロー解析を使用します。place が一方に含まれていて他方に含まれていない場合、 対象の初期化状態はコンパイル時に既知です(Dead または Static)。 この場合、drop の詳細化は対象にフラグを追加しません。単に Drop 終端命令を削除(Dead)または保持(Static)します。

Conditional drop については、変数全体の初期化状態が そのフィールドの初期化状態と同じであることがわかっています。したがって、その drop の対象に対する drop フラグを生成すれば、その対象の drop glue を呼び出しても安全です。

Open drop

Open drop は最も複雑です。単一の Drop 終端命令を、対象のフィールドのうち型が drop glue を持つもの (Ty::needs_drop)ごとに、それぞれ異なる複数の終端命令へ分解する必要があるためです。対象自体の drop glue を呼び出すことはできません。なぜなら、それには対象のすべてのフィールドが初期化されている必要があるからです。 カスタム Drop 実装を持つ型の変数では、そのフィールドからムーブできないため、Open drop は許可されないことを思い出してください。

これは、各フィールドを再帰的に DeadStaticConditional、または Open として分類することで実現されます。型が drop glue を持たないフィールドは 自動的に Dead になり、再帰の間に考慮する必要はありません。 種類が Open ではないフィールドに到達した場合は、上で行ったのと同様に処理します。その フィールドも Open である場合、再帰は継続します。 enum の Open ドロップをどのように扱うかは注目に値します。drop elaboration の内部では、 enum の各バリアントはフィールドのように扱われ、それらの「バリアントフィールド」のうち 任意の時点で初期化済みになれるのは 1 つだけである、という不変条件があります。 一般的な場合、どのバリアントがアクティブなものなのかはわからないため、enum のドロップグルー (判別子をチェックするもの)を呼び出すか、精緻化された Open ドロップの一部として 判別子を自分でチェックする必要があります。しかし、特定の場合(たとえば match アーム内)では、 enum のどのバリアントがアクティブかがわかっています。この情報は、非アクティブなバリアントに 対応するすべてのプレースを未初期化としてマークすることにより、MaybeInitializedPlacesMaybeUninitializedPlaces のデータフロー解析にエンコードされます。

クリーンアップパス

TODO: drop elaboration とアンワインドについて説明する。

余談: drop elaboration と const-eval

Rust では、コンパイル時の評価対象となる関数は、const キーワードを使って明示的に マークされていなければなりません。これには Drop トレイトの実装も含まれ、それらは const である場合もあれば、そうでない場合もあります。コンパイル時評価の対象となるコードは const 関数しか呼び出せないため、そのようなコード内での非 const な Drop 実装への呼び出しは 禁止されなければなりません。

Drop impl への呼び出しは、MIR では Drop ターミネーターとしてエンコードされます。しかし、 上で説明したように、新しく構築された MIR に含まれる Drop ターミネーターが、必ずしも Drop::drop の呼び出しになるとは限りません。その時点でドロップ対象が未初期化である可能性があります。 これは、新しく構築された MIR 上で非 const な Drop をチェックすると、偽のエラーが発生する可能性がある ことを意味します。その代わりに、drop elaboration が実行され、Dead ドロップ(対象が未初期化であることが わかっているもの)が削除されるまで待ってから、これらのチェックを実行します。