RustProgrammingRust 開発者

**ManuallyDrop<T>** と **MaybeUninit<T>** は、部分的に初期化されたデータ上でデストラクタの呼び出しを抑制する適合性に関してどのように異なるのか、並びに **ManuallyDrop** の内容を明示的にドロップした後に内部値にアクセスすることによって生じる特定の未定義動作について特定せよ。

Hintsage AIアシスタントで面接を突破

質問に対する答え

歴史。 ManuallyDrop<T> は、Rust 1.20 で自動デストラクタの呼び出しを防ぐために明示的に設計されたゼロコストラッパーとして登場し、部分的に初期化されたデータを扱ったり複雑なコンテナ型を実装したりする際に、より安全で意味的に明確な代替品として mem::forget の代わりに機能します。MaybeUninit<T> は、T の有効なインスタンスをまだ持っていないかもしれないメモリを管理しますが、ManuallyDrop は内部値が常に完全に初期化されていることを仮定し、その破壊のタイミングをプログラマーの裁量に委ねます。この区別は、コレクション型のためにカスタム Drop トレイトを実装する際に重要で、ManuallyDrop は生成時に二重ドロップエラーを引き起こさずにフィールドごとの抽出を許可し、Option<T> のランタイムオーバーヘッドを必要としません。

問題。 一般的なコンテナが破壊サイクル中に要素を排出する必要がある場合や、インプレース構築中にパニックから回復する必要がある場合、標準の Drop 実装では self から値を移動できず、コンパイラは Drop 実装が完了した後に移動した位置をドロップしようとします。Option<T>take() は安全な代替手段を提供しますが、ランタイムオーバーヘッド(判別ブール)を導入し、T を最初に Option として構築する必要があり、ゼロコスト抽象化の原則に違反します。ManuallyDrop は、T 自体と同じメモリレイアウトを持つコンパイル時保証されたラッパーを提供し、追加のメモリ割り当てや分岐ペナルティなしに ptr::read を介して直接フィールド抽出を可能にします。

解決策。 このラッパーは、#[repr(transparent)] 属性を通じて T のデストラクタの自動呼び出しを無効にし、デストラクタを実行するには明示的な不安全呼び出しを ManuallyDrop::drop に要求します。ヒープ割り当てされたリソースを含む構造体のために Drop を実装する場合、敏感なフィールドを ManuallyDrop にラップし、内部値の抽出を許可した後に手動でクリーンアップできます。drop を呼んだ後に内部値にアクセスすることは、値が論理的に初期化されていない状態になりますが、メモリに残っているため、ダングリングポインタが含まれている可能性があるため、即座に未定義動作を構成します。このパターンは、容量オーバーフローのために抽出に失敗した場合に要素ドロップを防ぎながら、バックストレージを解放する必要がある Vec::drop のようなゼロコスト抽象化に不可欠です。

use std::mem::ManuallyDrop; use std::ptr; struct Buffer<T> { // ヒープ割り当てへの生ポインタ ptr: *mut T, // ManuallyDrop で Vec を自動ドロップせずに取得できる temp_storage: ManuallyDrop<Vec<T>>, } impl<T> Drop for Buffer<T> { fn drop(&mut self) { // ManuallyDrop から安全に Vec を抽出する let vec = unsafe { ptr::read(&*self.temp_storage) }; // Vec の二重ドロップを防ぐための手動ドロップが必要 unsafe { ManuallyDrop::drop(&mut self.temp_storage) }; // これで self.temp_storage をもう一度ドロップしようとせずに vec を使える drop(vec); } }

実生活からの状況

問題の説明。 128KB RAM のマイクロコントローラで動作する高性能ロックフリキューを開発中、キューの Drop 実装中に重要な問題が発生しました。キューはノードが Box<Node<T>> ポインタを持つ侵入型連結リストを使用し、スタックオーバーフローを引き起こすことなく10,000以上のノードを排出する必要がありました。さらに、パニックが発生したときに一部のノードが中間初期化状態にある可能性があり、完全に初期化されたノードのみを選択的に破棄し、部分的に構築されたものは漏らす必要がありました。

解決策 1: Option と take を使用。 最初は各ノードポインタを Option<Box<Node<T>>> でラップし、while let Some(node) = head.take() を使ってリストを排出しました。 長所: 完全に安全で、イディオマティックな Rust、不安全なコードは不要で、メンテナンスも容易。 短所: 各ノードは Option の判別用に余分なバイトを持ち、埋め込みコンテキストで約12%メモリフットプリントが増加し、take() 操作はホットパスでの分岐予測ペナルティを導入し、ベンチマークでのスループットを8%低下させました。

解決策 2: mem::forget を使用。 自動ドロップを防ぐためにキュー全体の std::mem::forget を使用し、その後 alloc::dealloc で手動メモリを解放することを検討しました。 長所: 再帰的ドロップを防ぎ、Option のオーバーヘッドを回避しました。 短所: 非常に不安全で、Rust のアロケータの安全チェックをバイパスする手動メモリ管理が必要で、手動解放が失敗した場合にメモリが漏れ、今後の開発者が生ポインタ演算に不慣れなため、コードがメンテナンス不可能になりました。

解決策 3: ManuallyDrop フィールド。 Node 構造体を再設計し、next ポインタを ManuallyDrop<Box<Node<T>>> として格納しました。Drop 中に生ポインタ操作を使用してリストを繰り返し、各 Boxptr::read で抽出し、ローカル変数に移動し、ノードが完全に初期化されていることを原子状態フラグで確認した後にのみ、抽出されたスロット上で ManuallyDrop::drop を明示的に呼び出しました。 長所: ゼロメモリオーバーヘッド(ManuallyDrop#[repr(transparent)])、破壊順序を完全に制御可能、部分的に初期化されたノードを安全に処理する能力があり、初期化されていないノードの手動ドロップをスキップできます。 短所: unsafe ブロックの必要性とシニアエンジニアによる不変条件の慎重な監査が必要です。

どの解決策が選ばれたのか、なぜか。 我々は解決策 3(ManuallyDrop)を選択しました。なぜなら、埋め込みシステムの厳しいRAM制限が10,000ノードの容量要件に対して Option のオーバーヘッドを許容できなかったため、mem::forget は製品コードにはエラープローンすぎたからです。ManuallyDrop は、侵入型データ構造に必要な正確な制御を提供しつつ、Rust のメモリ安全性保証を維持できました。私は、不安全な操作を少数の十分にテストされたモジュールにラップし、テストビルドで不変条件を確認する debug_assertions を追加し、不安全性の不変条件を詳細に文書化しました。

結果。 キューは最大容量のチェーンをスタックオーバーフローなしに処理し、チェーン長にかかわらずメモリ使用量を一定に保ち、未定義動作の欠如を確認する Miri(中間表現インタプリタ)の検証に合格しました。明示的な手動ドロップ呼び出しは、破壊ロジックをコードレビュアーにとってすぐに見えるものとし、従来の C++ 実装で悩まされた微妙な二重ドロップバグを防止しました。