web-dev-qa-db-ja.com

最後の手段としてstd :: shared_ptr?

「Going Native 2012」のストリームを見ていると、std::shared_ptrに関する議論に気づきました。 Bjarneのstd::shared_ptrに対するいくらか否定的な見方と、オブジェクトの寿命が不確かな場合に「最後の手段」として使用すべきであるという彼のコメントを聞いて少し驚いた(彼によれば、ケースです)。

誰かがこれをもう少し詳しく説明したいと思いますか?どのようにstd::shared_ptrなしでプログラミングし、オブジェクトのライフタイムをsafeの方法で管理できますか?

62
ronag

所有権の共有を回避できる場合、アプリケーションはシンプルで理解しやすくなり、メンテナンス中に発生するバグの影響を受けにくくなります。所有権モデルが複雑または不明確であると、簡単に追跡できない共有状態を介して、アプリケーションのさまざまな部分の結合を追跡することが困難になる傾向があります。

このため、自動保存期間を持つオブジェクトを使用し、「値」サブオブジェクトを作成することをお勧めします。これに失敗すると、unique_ptrshared_ptr存在すること-最後の手段ではないにしても-望ましいツールのリストの下にある方法で。

58
CB Bailey

std::shared_ptrを使用したことがないと思います。

ほとんどの場合、オブジェクトは、その存続期間全体が属するコレクションに関連付けられています。その場合は、whatever_collection<o_type>またはwhatever_collection<std::unique_ptr<o_type>>を使用できます。そのコレクションは、オブジェクトまたは自動変数のメンバーです。もちろん、動的な数のオブジェクトが必要ない場合は、固定サイズの自動配列を使用できます。

コレクションの反復やオブジェクトに対するその他の操作のいずれも、所有権を共有するためのヘルパー関数を必要としません...それオブジェクトを使用してから戻り、呼び出し元は、オブジェクトが呼び出し全体にわたって存続することを保証します 。これは、呼び出し元と呼び出し先の間で最も使用される契約です。


Nicol Bolasは、「あるオブジェクトがネイキッドポインターをつかんでいると、そのオブジェクトが死ぬと……おっと」とコメントしました。および「オブジェクトは、オブジェクトがそのオブジェクトの存続期間にわたって存続することを保証する必要があります。それを実行できるのはshared_ptrのみです。」

私はその議論を買いません。少なくともshared_ptrはこの問題を解決しません。何について:

  • いくつかのハッシュテーブルがオブジェクトを保持し、そのオブジェクトのハッシュコードが変更された場合...おっと。
  • ある関数がベクトルを反復していて、要素がそのベクトルに挿入されている場合...おっと。

ガベージコレクションと同様に、shared_ptrをデフォルトで使用すると、プログラマーはオブジェクト間、または関数と呼び出し元の間のコントラクトについて考える必要がなくなります。正しい前提条件と事後条件について考える必要があり、オブジェクトの存続期間はその大きなパイのほんの一部です。

オブジェクトは「死ぬ」ことはなく、コードの一部がそれらを破壊します。そして、コールコントラクトを理解するのではなく、問題にshared_ptrを投げることは誤った安全です。

39
Ben Voigt

私は(「最後の手段」のように)絶対的な用語で考えるのではなく、問題の領域に関連して考えます。

C++は、寿命を管理するためのさまざまな方法を提供できます。それらのいくつかは、スタック駆動の方法でオブジェクトを再変換しようとします。他のいくつかはこの制限を回避しようとします。それらのいくつかは「リテラル」であり、他のいくつかは近似です。

実際にできること:

  1. 純粋な値のセマンティクスを使用。重要なのは「ID」ではなく「値」である比較的小さなオブジェクトで機能します。同じPersonを持つ2つのnameは同じであると想定できますperson(より良い:同じpersonの2つの表現)。ライフタイムはマシンスタックによって付与され、プログラムの終了は本質的に重要ではありません((personなので)namePersonが何を持っているかに関係なく)
  2. スタックに割り当てられたオブジェクトを使用、および関連する参照またはポインタ:ポリモーフィズムを許可し、オブジェクトの存続期間を付与します。 「スマートポインター」の必要はありません。これは、スタック内にあるオブジェクトよりも長くスタックに残る構造によってオブジェクトが「ポイント」されないようにするためです(最初にオブジェクトを作成し、次にそれを参照する構造を作成します)。
  3. スタック管理ヒープ割り当てオブジェクトを使用:これはstd :: vectorとすべてのコンテナーが行うことであり、wat _std::unique_ptr_が行う(サイズ1のベクターと考えることができます)。繰り返しますが、オブジェクトが参照するデータ構造の前(後)にオブジェクトが存在し始める(そして存在しなくなる)ことを認めます。

このメソッドの弱点は、オブジェクトのタイプと数量が、それらが作成される場所に関して、より深いスタックレベルの呼び出しの実行中に変更できないことです。これらすべての手法は、オブジェクトの作成と削除がユーザーアクティビティの結果であるすべての状況でその強みを「失敗」させます。その結果、オブジェクトのランタイムタイプはコンパイル時に認識されず、オブジェクトを参照する構造が過剰になる可能性があります。ユーザーは、より深いスタックレベルの関数呼び出しから削除することを求めています。この場合、次のいずれかを行う必要があります。

  • オブジェクトおよび関連する参照構造の管理に関するいくつかの分野を紹介するか、または...
  • どういうわけか「純粋なスタックベースのライフタイムをエスケープする」のダークサイドに行きましょう:オブジェクトはそれらを作成した関数とは無関係に離れなければなりません。そして、必要になるまで...を残しておく必要があります

C++ isteslfには、そのイベントを監視するネイティブメカニズム(while(are_they_needed))がないため、次のように概算する必要があります。

  1. 共有所有権を使用:オブジェクトの寿命は「参照カウンター」にバインドされます:「所有権」を階層的に整理できる場合は機能し、所有権ループが存在する可能性がある場合は失敗します。これはstd :: shared_ptrが行うことです。また、weak_ptrを使用してループを解除できます。これはほとんどの場合機能しますが、大規模な設計では失敗します。多くの設計者は異なるチームで作業し、誰が何を所有しているのかについての明確な理由(多少の要件によるもの)はありません(典型的な例は二重好きチェーンです:は以前は次を参照するか、次は前を参照するか、次は前を参照して次を所有しますか?要件が存在しない場合、これらのソリューションは同等であり、大規模なプロジェクトではそれらを混同するリスクがあります)
  2. ガベージコレクションヒープを使用します:単に生涯の気にしないでください。あなたは時々コレクターを実行し、unreachabeは「もう必要ない」と見なされ、そして...ええと...へへ...破壊されましたか?確定?フローズン?。 GCコレクターは多数ありますが、実際にはC++に対応しているコレクターは見つかりません。それらのほとんどは、オブジェクトの破壊を気にせず、メモリを解放します。
  3. C++対応のガベージコレクターを使用、適切な標準メソッドインターフェイス。それを見つける幸運。

最初の解決策から最後の解決策に進むと、オブジェクトの寿命を管理するために必要な補助データ構造の量は、オブジェクトを整理して維持するために費やす時間が増えるにつれて増加します。

ガベージコレクターはコストがかかり、shared_ptrは少なく、unique_ptrはさらに少なく、スタック管理オブジェクトはほとんどありません。

_shared_ptr_は「最後の手段」ですか?いいえ、そうではありません。最後の手段はガベージコレクタです。 _shared_ptr_は、実際には_std::_が提案する最後の手段です。しかし、あなたが私が説明した状況にいるなら、適切な解決策かもしれません。

16

後のセッションでハーブサッターが言及したことの1つは、shared_ptr<>をコピーするたびに、連動するインクリメント/デクリメントが発生することです。マルチコアシステムのマルチスレッドコードでは、メモリの同期は重要ではありません。選択肢を考えると、スタック値またはunique_ptr<>のいずれかを使用して、参照または生のポインタを渡すのが適切です。

9
Eclipse

最後の「リゾート」が彼が使用した正確な言葉であったかどうかは覚えていませんが、彼の言ったことの実際の意味は最後の「選択」だったと思います。 unique_ptr、weak_ptr、shared_ptr、さらにはネイキッドポインターさえもその位置を占めています。

彼ら全員が同意したことの1つは、私たち(開発者、本の作者など)がすべてC++ 11の「学習段階」にあり、パターンとスタイルが定義されていることです。

例として、ハーブは Effective C++ (Meyers)や C++ Coding Standards (Sutter&Alexandrescu)などの精巧なC++書籍の新版を期待すべきだと説明しました。 C++ 11での業界の経験とベストプラクティスが実現するまでの数年。

7
Eddie Velasquez

彼が得ているのは、標準的なポインタ(一種のグローバル置換のような)を書いた可能性があるときはいつでも、誰もがshared_ptrを書くことが一般的になりつつあり、実際に設計するか、少なくともオブジェクトの作成と削除の計画。

人々が忘れているもう1つのことは(上記の資料で述べたロック/更新/ロック解除のボトルネック以外に)、shared_ptrだけではサイクルの問題を解決できないことです。 shared_ptrでリソースをリークすることができます:

オブジェクトA、別のオブジェクトAへの共有ポインターを含むオブジェクトBはA a1とA a2を作成し、a1.otherA = a2を割り当てます。およびa2.otherA = a1;これで、a1、a2を作成するために使用したオブジェクトBの共有ポインターがスコープ外になります(関数の最後など)。これでリークがあります-他の誰もa1とa2を参照していませんが、お互いを参照しているため、参照カウントは常に1であり、リークしました。

これは単純な例です。これが実際のコードで発生すると、通常は複雑な方法で発生します。 weak_ptrを使用した解決策がありますが、非常に多くの人々が現在、どこでもshared_ptrを実行しており、リークの問題や、weak_ptrさえも知りません。

まとめると、OPが参照するコメントは次のように要約されます。

使用している言語(管理対象、非管理対象、またはshared_ptrのような参照カウントとの中間)に関係なく、オブジェクトの作成、ライフタイム、破棄を理解し、意図的に決定する必要があります。

編集:それが「不明、私はshared_ptrを使用する必要がある」という意味であっても、あなたはまだそれを考えており、意図的にそうしています。

5
anon

allオブジェクトが参照カウントされ、ヒープに割り当てられる言語であるObjective-Cでの経験から答えます。オブジェクトを処理する方法が1つしかないため、プログラマにとってははるかに簡単です。これにより、標準のルールを定義することが可能になり、準拠すると、コードの堅牢性が保証され、メモリリークが発生しなくなります。また、最近のARC(自動参照カウント)のように、巧妙なコンパイラーの最適化が可能になりました。

私のポイントは、shared_ptrが最後の手段ではなく、最初のオプションであるべきだということです。デフォルトでの参照カウントとその他のオプションは、自分が何をしているのかがはっきりしている場合にのみ使用してください。生産性が向上し、コードがより堅牢になります。

3
Dimitris

私は質問に答えようとします:

どのようにしてstd :: shared_ptrなしでプログラミングし、オブジェクトのライフタイムを安全に管理できますか?

C++には、メモリを実行するさまざまな方法が多数あります。次に例を示します。

  1. クラススコープでshared_ptrの代わりに_struct A { MyStruct s1,s2; };_を使用します。依存関係がどのように機能するかを理解する必要があり、依存関係をツリーに制限するのに十分な依存関係を制御する機能が必要であるため、これは上級プログラマ向けです。ヘッダーファイル内のクラスの順序は、これの重要な側面です。この使用法は、ネイティブの組み込みc ++型では既に一般的ですが、プログラマー定義クラスでの使用は、これらの依存関係とクラスの順序の問題のため、あまり使用されていないようです。このソリューションは、sizeofにも問題があります。プログラマーはこの問題を、前方宣言または不要な#includeを使用する必要があると見なしているため、多くのプログラマーは、ポインターの劣ったソリューションにフォールバックし、後でshared_ptrにフォールバックします。
  2. MyClass &find_obj(int i);の代わりにshared_ptr<MyClass> create_obj(int i); + clone()を使用してください。多くのプログラマーは、新しいオブジェクトを作成するためのファクトリーを作成したいと考えています。 shared_ptrは、この種の使用法に最適です。問題は、単純なスタックやオブジェクトベースのソリューションではなく、ヒープ/フリーストアの割り当てを使用する複雑なメモリ管理ソリューションをすでに想定していることです。優れたC++クラス階層は、そのうちの1つだけでなく、すべてのメモリ管理スキームをサポートします。参照ベースのソリューションは、ローカル関数スコープ変数を使用する代わりに、返されたオブジェクトが包含オブジェクト内に格納されている場合に機能します。所有権をファクトリからユーザーコードに渡すことは避けてください。 find_obj()を使用した後にオブジェクトをコピーすることは、オブジェクトを処理するための良い方法です。通常のコピーコンストラクターと、異なるパラメーターの通常のコンストラクターを使用して、多相性オブジェクトのrefrerenceパラメーターまたはclone()で処理できます。
  3. ポインターまたはshared_ptrsの代わりに参照を使用します。すべてのc ++クラスにはコンストラクターがあり、各参照データメンバーを初期化する必要があります。この使用法により、ポインターおよびshared_ptrsの多くの使用を回避できます。メモリがオブジェクトの内部か外部かを選択し、その決定に基づいて構造体ソリューションまたは参照ソリューションを選択するだけです。このソリューションの問題は通常、一般的だが問題のあるプラクティスであるコンストラクターパラメーターの回避と、クラスのインターフェイスの設計方法の誤解に関連しています。
1
tp1