Copyトレイトは、リソース管理の懸念なしに単純なビット単位のコピーで複製できる型を示すマーカーとして、Rustの初期設計に起源を持っています。Dropは、ファイルディスクリプタやヒープメモリのような外部リソースを管理する型のための決定論的なリソースクリーンアップを扱うために導入されました。暗黙の複製と唯一の所有権の間の対立は、ビット単位のコピーが共有できないリソースハンドルを共有することが判明したときに明らかになりました。その結果、コンパイラは両方のトレイトを同時に実装しようとする型を拒否するように設計されました。
もしDropを実装する型(例えば、ファイルディスクリプタを管理するもの)がCopyでもあれば、その値を新しい変数に割り当てることで、2つのビット単位で同一のコピーが作成されます。両方のコピーがスコープを出ると、カスタムDrop実装が同じ基盤リソースに対して2回実行されます。これにより、ダブルフリーの脆弱性や、最初のドロップによってリソースが無効化された場合に発生する使用後解放が引き起こされ、メモリ安全性が損なわれます。
Rustコンパイラは、トレイトシステムにおいてCopyとDropの両方を実装することを明示的に禁止する整合性チェックを含んでいます。この制約により、開発者はカスタム破棄を必要とする型に対してClone(明示的な複製)を使用させることが強制され、実装が適切に参照カウントをインクリメントしたり、深いコピーを実行することが可能になります。論理的な実体の各々に対して対応するユニークなドロップを持つことを保証することで、型システムは安全性の保証を犠牲にすることなく、ゼロコストの抽象化を維持します。
外部Cライブラリの接続オブジェクトへの生ポインタをラップするDatabaseHandle構造体を考えてみてください。アプリケーションは、ログ purposesのために複数のクロージャに値を渡す必要がありますが、各ハンドルはドロップ時にFFI呼び出しを介して独自の接続を閉じる必要があります。もしハンドルがCopyであれば、暗黙の複製は同じ基盤Cリソースの所有権を主張する複数のハンドルを生成し、スコープが終了したときにダブルクローズまたは使用後解放を引き起こすことになります。
一つのアプローチは、Copyを許可し、Arcを使用して内部参照カウントを実装することでした。これにより、すべてのハンドルに対して同期のオーバーヘッドが追加され、すべての操作におけるバイナリサイズとランタイムコストが増加しました。また、生ポインタをArcから原子的に抽出する必要があるFFI境界が複雑になり、ドロップロジック自身がRustコードに戻る場合、潜在的なデッドロックが発生する可能性もあります。
別のアプローチは、Copyを使用するが、値がドロップされる前にユーザーが手動でcloseメソッドを呼び出す必要があることを文書化することでした。これは、メモリの安全性の負担を完全にプログラマーに置くことになり、Rustのコンパイル時にエラーを防ぐというコア原則に違反します。開発者がcloseを呼び出すのを忘れた場合にはリソースリークが発生し、無意識にハンドルをコピーして両方のコピーを閉じようとする場合にはダブルクローズに繋がります。
選ばれた解決策は、Copyを削除し、Cloneを手動で実装するとともにDropを実装することでした。Cloneは、新しいデータベース接続を開くことにより深いコピーを実行し、各インスタンスが独自のリソースを所有し、基盤のCポインタのエイリアシングを防ぎます。Dropは自分の接続のみを閉じ、コンパイラは偶発的なビット単位のコピーを防ぎ、安全性を維持しつつランタイムオーバーヘッドを必要としません。
型システムは、コンパイル時に偶発的なコピーを防ぎ、開発者に明示的にcloneを呼び出すことを強制し、リソース取得をソースコード内で可視化します。プログラムは、ハンドルがスレッドやクロージャに渡される際にダブルフリーエラーを回避し、決定論的な破壊保証を維持でき、原子操作や手動のメモリ管理を必要としません。
Vecを含む構造体に対してなぜCopyを導出できないのか?
Vecはヒープに割り当てられたメモリを所有し、ベクタがスコープを出るときにそのメモリを解放するためにDropを実装しています。Vecを含む構造体がCopyであった場合、ビット単位の複製はスタック上の同じヒープバッファを指す2つの構造体を作成しますが、両方ともヒープへの同じポインタを含んでいます。最初の構造体がドロップされたとき、メモリは解放され、次の構造体がドロップされたときには同じメモリを再び解放しようとし、未定義の動作が引き起こされます。Rustは、Copy型のすべてのフィールドもCopyであることを要求することにより、再帰的にネストされたDrop実装が存在しないことを保証します。
mem::forgetはCopyとDropの問題を解決しますか?
std::mem::forgetは、値を消費する際にそのデストラクタを実行しませんが、特定の所有値にのみ影響し、すべてのコピーには影響しません。CopyとDropが許可されていれば、1つのコピーを忘れることが他のビット単位のコピーがスコープを出たときにDrop実装を実行するのを防ぐことはできません。それらの残りのドロップも依然として同じ基盤リソースを解放しようとし、使用後解放やダブルフリーが発生します。
ManuallyDropを使用してCopyを安全に実装できますか?
フィールドをManuallyDropでラップすることで、Dropの自動呼び出しを防ぐことができ、技術的には外部構造体がCopyを導出することを可能にします。しかし、これはユーザーが作成したすべてのコピーに対してManuallyDrop::dropを呼び出す責任を負わせ、実質的には手動メモリ管理のシナリオを生み出します。ユーザーがたった1つのコピーでもドロップを忘れた場合、リソースは永続的にリークします。Rustは、決定論的かつ自動的なクリーンアップの安全性保証を損なうため、リソースを所有する型に対してこのパターンを禁止しています。