共有ポインターは、そのオブジェクトを指しているポインターの数をどのように知るのでしょうか? (この場合は shared_ptr)
5 に答える
基本的に、shared_ptr
2つのポインターがあります。共有オブジェクトへのポインターと2つの参照カウントを含む構造体へのポインターです。1つは「強い参照」または所有権を持つ参照用で、もう1つは「弱い参照」またはそうでない参照用です。所有権を持っています。
をコピーするshared_ptr
と、コピーコンストラクターは強力な参照カウントをインクリメントします。を破棄するshared_ptr
と、デストラクタは強力な参照カウントをデクリメントし、参照カウントがゼロかどうかをテストします。そうである場合、デストラクタは共有オブジェクトを削除します。これは、共有オブジェクトshared_ptr
をポイントしなくなったためです。
弱参照カウントはweak_ptr
、;をサポートするために使用されます。基本的に、からaweak_ptr
が作成されるたびshared_ptr
に、弱参照カウントがインクリメントされ、1つが破棄されるたびに、弱参照カウントがデクリメントされます。強参照カウントまたは弱参照カウントのいずれかがゼロより大きい限り、参照カウント構造体は破棄されません。
事実上、強力な参照カウントがゼロより大きい限り、共有オブジェクトは削除されません。強参照カウントまたは弱参照カウントがゼロでない限り、参照カウント構造体は削除されません。
私は一般的にジェームズ・マクネリスの答えに同意します。ただし、もう 1 つ注意すべき点があります。
ご存知shared_ptr<T>
かもしれませんが、型T
が完全に定義されていない場合にも使用できます。
あれは:
class AbraCadabra;
boost::shared_ptr<AbraCadabra> myPtr;
// ...
これはコンパイルして動作します。スマート ポインターの他の多くの実装とは異なり、実際にはカプセル化された型を使用するために完全に定義する必要があります。これは、スマート ポインターが、カプセル化されたオブジェクトが参照されなくなったときにそのオブジェクトを削除することを知っているはずであるという事実に関連しています。オブジェクトを削除するには、それが何であるかを知る必要があります。
これは、次のトリックによって実現されます。shared_ptr
実際には、次のもので構成されます。
- オブジェクトへの不透明なポインタ
- 共有参照カウンター (James McNellis が説明したもの)
- オブジェクトを破棄する方法を知っている、割り当てられたファクトリへのポインター。
上記のファクトリは、オブジェクトを正しい方法で削除することになっている単一の仮想関数を持つヘルパー オブジェクトです。
このファクトリは、共有ポインタに値を割り当てるときに実際に作成されます。
つまり、次のコード
AbraCadabra* pObj = /* get it from somewhere */;
myPtr.reset(pObj);
これは、このファクトリが割り当てられている場所です。注:reset
関数は実際にはテンプレート関数です。実際には、指定された型 (パラメーターとして渡されたオブジェクトの型) のファクトリを作成します。ここで、タイプを完全に定義する必要があります。つまり、まだ定義されていない場合、コンパイル エラーが発生します。
また、派生型 (から派生AbraCadabra
) のオブジェクトを実際に作成し、それをに割り当てるとshared_ptr
、デストラクタが仮想でなくても正しい方法で削除されます。は、関数shared_ptr
で見られるタイプに従って、常にオブジェクトを削除しreset
ます。
そのため、shared_ptr はスマート ポインターの非常に洗練されたバリアントです。それは素晴らしい柔軟性を提供します。ただし、この柔軟性は、スマート ポインターの他の可能な実装と比較して、パフォーマンスが非常に悪いという代償を伴うことを知っておく必要があります。
一方、いわゆる「侵入型」スマート ポインターがあります。それほど柔軟性はありませんが、対照的に最高のパフォーマンスを発揮します。
shared_ptr
侵入型スマート ポインターと比較した利点:
- 非常に柔軟な使い方。に割り当てるときにカプセル化された型を定義するだけで済みます
shared_ptr
。これは大きなプロジェクトにとって非常に価値があり、依存関係を大幅に減らします。 - カプセル化された型は仮想デストラクタを持つ必要はありませんが、ポリモーフィック型は正しく削除されます。
- 弱いポインターで使用できます。
shared_ptr
侵入型スマート ポインターと比較した場合の短所:
- 非常に野蛮なパフォーマンスとヒープメモリの浪費。割り当て時に、参照カウンターとファクトリ (メモリの無駄、遅い) の 2 つのオブジェクトをさらに割り当てます。ただし、これは でのみ発生し
reset
ます。1 つshared_ptr
が別のものに割り当てられると、それ以上は割り当てられません。 - 上記は例外をスローする場合があります。(メモリ不足状態)。対照的に、侵入型スマート ポインターは決してスローしない可能性があります (無効なメモリ アクセス、スタック オーバーフローなどに関連するプロセス例外は別として)。
- オブジェクトの削除も遅い: 別の 2 つの構造体の割り当てを解除する必要があります。
- 侵入型のスマート ポインターを使用する場合、スマート ポインターと未加工のポインターを自由に混在させることができます。実際の参照カウントは単一のオブジェクト自体の内部に存在するため、これは問題ありません。対照的に、with は生のポインターと混在さ
shared_ptr
せることはできません。
AbraCadabra* pObj = /* get it from somewhere */;
myPtr.reset(pObj);
// ...
pObj = myPtr.get();
boost::shared_ptr<AbraCadabra> myPtr2(pObj); // oops
上記はクラッシュします。
少なくとも 3 つのよく知られたメカニズムがあります。
外部カウンター
オブジェクトへの最初の共有ポインターが作成されると、別の参照カウント オブジェクトが作成され、1 に初期化されます。ポインターがコピーされると、参照カウントが増加します。ポインターが破棄されると、ポインターは減少します。ポインターの割り当ては、1 つのカウントを増やし、別のカウントを減らします (この順序で行わないと、自己割り当てptr=ptr
が壊れます)。参照カウントが 0 になると、ポインターが存在しなくなり、オブジェクトが削除されます。
内部カウンター
内部カウンターでは、指しているオブジェクトにカウンター フィールドが必要です。これは通常、特定の基本クラスから派生することによって実現されます。代わりに、これにより参照カウントのヒープ割り当てが節約され、生のポインターから共有ポインターを繰り返し作成できます (外部カウンターを使用すると、1 つのオブジェクトに対して 2 つのカウントが発生します)。
循環リンク
カウンターを使用する代わりに、オブジェクトへのすべての共有ポインターを循環グラフに保持できます。作成された最初のポインターは、それ自体を指します。ポインターをコピーすると、そのコピーが円に挿入されます。削除すると、サークルから削除されます。しかし、破棄されたポインターがそれ自体を指している場合、つまりそれが唯一のポインターである場合は、指しているオブジェクトを削除します。
欠点は、循環単一リンク リストからノードを削除すると、先行ノードを見つけるためにすべてのノードを反復処理する必要があるため、かなりコストがかかることです。これは、参照の局所性が低いため、特に苦痛になる可能性があります。
バリエーション
2 番目と 3 番目のアイデアを組み合わせることができます。基本クラスは、カウントを含む代わりに、その循環グラフの一部にすることができます。もちろん、これはオブジェクトがそれ自体を指している場合にのみ削除できることを意味します (サイクル長 1、オブジェクトへのポインターが残っていない)。繰り返しになりますが、ウィーク ポインターからスマート ポインターを作成できるという利点がありますが、チェーンからポインターを削除する際のパフォーマンスの低下は依然として問題です。
アイデア 3 の正確なグラフ構造はあまり重要ではありません。また、ポイント先のオブジェクトをルートとするバイナリ ツリー構造を作成することもできます。繰り返しになりますが、難しい操作は、そのグラフから共有ポインター ノードを削除することです。利点は、多くのスレッドに多くのポインターがある場合、グラフの一部を大きくすることは、高度に競合する操作ではないことです。
「共有ポインターは、オブジェクトへのポインターと共有参照カウントへのポインターを保持するスマートポインター (オーバーロードされた operator*() および operator->() を持つ C++ オブジェクト) です。スマートポインターのコピーが作成されるたびにコピー コンストラクタを使用すると、参照カウントが増加します。共有ポインタが破棄されると、そのオブジェクトの参照カウントが減少します。生のポインタから構築された共有ポインタは、最初は参照カウントが 1 です。参照カウントが 0 に達すると、ポイントされたオブジェクトが破棄され、それが占有していたメモリが解放されます。オブジェクトを明示的に破棄する必要はありません。最後のポインタのデストラクタが実行されると、自動的に破棄されます。"ここから.
これらは、shared_ptr コピー コンストラクター/代入演算子でインクリメントされ、デストラクタでデクリメントされる内部参照カウントを保持します。カウントがゼロになると、保持されたポインターは削除されます。
スマート ポインターに関するBoost ライブラリのドキュメントは次のとおりです。TR1 の実装は とほとんど同じだと思いますboost::shared_ptr
。