参照/ポインタとIDを使用することの長所と短所は何ですか
私はC ++で書いていますが、この問題はGCのないすべての言語に当てはまり、GCのある言語にも当てはまります。
オブジェクトを作成/追加する構造がメモリ内にあります。構造はそれらのオブジェクトの所有権を取得します。構造から削除された後は、オブジェクトを使用する必要はありません。
このデータ構造を最初に実装したとき、そこに格納されているオブジェクトにID /キー/名前/ハンドルを使用するのは自然なことのように思われました。私はそれを次のように使用しています:
id1 = structure.addObj(new Square());
id2 = structure.addObj(new Square());
id3 = structure.addObj(new Circle());
obj3 = structure.getObj(id3);
obj3.addFriend( id1 );
obj3.addFriend( id2 );
idMax = structure.findObjWithMostFriends();
objMax = structure.getObj(idMax);
print(objMax.name);
しばらく使ってみたら、IDを忘れて、常にオブジェクトへの参照を使うほうがいいと思います。このようにして、毎回構造体への参照を渡す必要はありません。
参照のみを使用するようにすべてをリファクタリングすることを考えていますが、後悔することを恐れています。IDを使用して続行するかどうかを決定することの長所と短所について詳しく知りたいです。
メモリの詳細:
オブジェクトはヒープに割り当てられ、アドレスは変更されません。
構造体は、それらが削除されたときにそれらのオブジェクトの割り当てを解除します(代わりに、呼び出し元に解放される可能性がありますが、現時点ではこれは必要ありません)。
構造に属していないオブジェクトを使用することは想定されていません。私のプログラムが正しければ、IDやポインタがぶら下がってしまうことはありません。ただし、プログラムにバグがある場合に発生する可能性があります。
IDから同様の問題の参照に切り替えた経験は何ですか?どのソリューションを使用する必要がありますか?
回答
ポインタとIDの長所と短所は、それらが使用されるコンテキストによって異なります。したがって、一般的な推奨事項は不可能です。
通常、IDは、リポジトリなどの「コンテナ」、または所有する集合体として機能するオブジェクトに関連付けられている場合に意味があります。この場合、idを使用すると、メモリレイアウトから抽象化し、よりカプセル化されたインターフェイスを定義し、コンテナを簡単にシリアル化できます。グラフのコンテキストでは、グラフ全体がそのノードとそのエッジの「コンテナ」になるのに適しています。
ただし、グローバルにIDを使用し始めると、gloablのデフォルトコンテナを想定することで、基盤となるグローバル構造に緊密に結合され、再利用が難しく、長期的に維持するのが難しいコードを構築していることになります。この問題の最初の影響は、使用している混合APIの種類です。
- オブジェクトの参照を使用してオブジェクトを変更する必要がありますが、IDを使用してオブジェクトを作成するか、引数として渡す必要があります。
- IDに関連付けられた参照をリークすると、抽象化するメモリレイアウトにバインドされ、コンテキストは、オブジェクトの移動を禁止する参照を追跡する可能性があります(および、の場合はポインタがぶら下がるリスクがあります)。バグ)。
- オブジェクトの一時的なコピーを作成すると、最終的にはグローバル構造になります。
これはすべて、そのままではエラーが発生しやすいように思えます。特に、最新のauto
スタイルを使用して変数を宣言するときに、参照とIDが混同されるリスクを追加する場合。
結論:APIを完全にリエンジニアリングして完全にIDベースにし、コンテナー(つまり、現在のグローバル構造)を明示的にするかshared_ptr
、IDの代わりに一貫性のあるAPIに切り替えます。IDは役立つ要素にすぎません。不明な場合にshare_ptrを見つけるため(およびデータをシリアル化するため)。
IDまたはハンドルは、通常、次の場合に適しています。
- C ++標準ライブラリコンテナを指す場合やプロセス間でオブジェクトを転送する場合など、メモリの場所が修正されない可能性があります
- ポインタのアドレス空間と比較してIDが少ないことがわかっています(メモリ要件を大幅に削減できます)
- オブジェクトは参照カウントを介して管理され、オブジェクトグラフにはサイクルがある場合があります
- 間接参照が必要ですが、言語はファーストクラスのポインター(Python、Javaなど)をサポートしていません
- オブジェクトの存続期間、たとえばオブジェクトグラフ全体の決定論的な割り当て解除や解放後の使用の脆弱性について懸念している
生涯のポイントが重要です。C / C ++では、ポインタを逆参照できるように、ポイントされたオブジェクトがまだ生きているかどうかを知るのはあなたの責任です。これに対処するには、2つの戦略があります。参照カウントまたはGCを使用して、ポインターがある限りオブジェクトを存続させるか、Rustコンパイラーのように存続期間を慎重に検討します(偶然にも複雑なオブジェクトグラフにIDを使用する必要があります)。
IDだけでは逆参照できないため、IDは存続期間の問題に対する部分的な解決策ですが、実際のオブジェクトグラフを含むコンテキストが必要です。このコンテキストの存続期間は、特にコンテキストがスタックに割り当てられたオブジェクトによって表され、ヒープに割り当てられたオブジェクトから直接参照されない場合は、通常、推論が容易です。
ただし、これは気密ではありません。たとえば、間違ったコンテキストでIDを逆参照する可能性があります。ここでも、2つのアプローチがあります。ID解決が失敗する可能性があるため、解決関数からnull許容ポインターが返されるか、このエラーを検出しようとします。各コンテキストに短いIDを割り当て、それを各オブジェクトID /ハンドルにエンコードすることで、検出の可能性を高めることができます。
私は現在、スマートポインターベースのシステムをIDベースのシステムに移行することを考えています。これにより、オブジェクトグラフを介したより豊富なクエリが実行可能になり、参照カウントのオーバーヘッドをなくすことができるからです。ただし、IDが再利用される可能性があると、バグの検出が困難になる可能性があります(これも一種の解放後使用)。
IDは永続性のために非常にうまく機能します。したがって、データベースにIDを格納し、IDを指定してオブジェクトを提供するためのAPIがあります。複数の呼び出しで同じオブジェクトである場合は、非常に望ましいです。これは主に、永続ストレージからアイテムを読み取るときに使用します。
メモリに入ると、参照カウントポインタ(C ++では共有ポインタ)を使用する方がはるかに簡単で効率的です。