スマートポインターとその避けられない不確定性に関する質問
-
03-07-2019 - |
質問
過去2年間、プロジェクトでスマートポインター(正確にはboost :: shared_ptr)を広範囲に使用しています。私は彼らの利益を理解し、感謝しています。私は一般的に彼らがとても好きです。しかし、それらを使用すればするほど、プログラミング言語で私が好むと思われるメモリ管理とRAIIに関して、C ++の決定論的な動作を見逃します。スマートポインターは、メモリ管理のプロセスを簡素化し、とりわけ自動ガベージコレクションを提供しますが、問題は、自動ガベージコレクション全般とスマートポインターを使用すると、(デ)初期化の順序である程度の不確定性が明確に導入されることです。この不確定性はプログラマーからコントロールを奪い、最近気づいたように、APIの設計と開発の仕事をします。APIの使用は、開発時に事前に完全には知られていません。すべての使用パターンとコーナーケースを十分に考慮する必要があります。
さらに詳しく説明するため、現在APIを開発しています。このAPIの一部では、特定のオブジェクトを他のオブジェクトの前に初期化または破棄する必要があります。別の言い方をすれば、(初期化)(初期化)の順序は時々重要です。簡単な例を示すために、Systemというクラスがあるとします。システムはいくつかの基本的な機能(この例ではロギング)を提供し、スマートポインターを介して多くのサブシステムを保持します。
class System {
public:
boost::shared_ptr< Subsystem > GetSubsystem( unsigned int index ) {
assert( index < mSubsystems.size() );
return mSubsystems[ index ];
}
void LogMessage( const std::string& message ) {
std::cout << message << std::endl;
}
private:
typedef std::vector< boost::shared_ptr< Subsystem > > SubsystemList;
SubsystemList mSubsystems;
};
class Subsystem {
public:
Subsystem( System* pParentSystem )
: mpParentSystem( pParentSystem ) {
}
~Subsystem() {
pParentSubsystem->LogMessage( "Destroying..." );
// Destroy this subsystem: deallocate memory, release resource, etc.
}
/*
Other stuff here
*/
private:
System * pParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
};
すでにおわかりのように、サブシステムはシステムのコンテキストでのみ意味があります。しかし、そのような設計のサブシステムは、その親システムより簡単に存続できます。
int main() {
{
boost::shared_ptr< Subsystem > pSomeSubsystem;
{
boost::shared_ptr< System > pSystem( new System );
pSomeSubsystem = pSystem->GetSubsystem( /* some index */ );
} // Our System would go out of scope and be destroyed here, but the Subsystem that pSomeSubsystem points to will not be destroyed.
} // pSomeSubsystem would go out of scope here but wait a second, how are we going to log messages in Subsystem's destructor?! Its parent System is destroyed after all. BOOM!
return 0;
}
サブシステムを保持するために生のポインタを使用していた場合、システムがダウンしたときにサブシステムを破壊していました。もちろん、pSomeSubsystemはダングリングポインタになります。
クライアントプログラマを自分自身から保護するのはAPIデザイナーの仕事ではありませんが、APIを簡単に正しく使用し、誤って使用しにくいようにすることをお勧めします。だから皆さんにお願いします。どう思いますか?この問題をどのように軽減する必要がありますか?そのようなシステムをどのように設計しますか?
事前に感謝します、 ジョシュ
解決
問題の要約
この質問には2つの競合する懸念があります。
-
サブシステム
のライフサイクル管理。適切なタイミングで削除できます。 -
サブシステム
のクライアントは、使用しているサブシステム
が有効であることを知る必要があります。
処理#1
System
は Subsystem
を所有しており、独自のスコープでライフサイクルを管理する必要があります。このために shared_ptr
を使用すると、破壊が簡単になるため、特に便利ですが、割り当てを解除するために求めている決定性を失うため、それらを配布しないでください。
処理#2
これは、より興味深い関心事です。問題をより詳細に説明すると、クライアントは Subsystem
(およびその親 System
)が存在する間に Subsystem
のように動作するオブジェクトを受け取る必要があります。 、ただし Subsystem
が破棄された後は適切に動作します。
これは、プロキシパターンの組み合わせによって簡単に解決できます、 State Pattern およびヌルオブジェクトパターン。これは少し複雑なソリューションのように思えるかもしれませんが、「複雑さの反対側にあるのは単純さ」だけです。ライブラリ/ API開発者として、システムを堅牢にするためにさらに努力する必要があります。さらに、システムがユーザーの期待どおりに直感的に動作し、ユーザーがシステムを誤用しようとすると優雅に減衰するようにしたいと考えています。この問題には多くの解決策がありますが、あなたと Scott Meyers は、「正しく使用するのは簡単で、間違って使用するのは難しい」と言います。 '
今、実際には、 System
は Subsystem
の基本クラスを処理し、そこからさまざまな Subsystem
を派生させると仮定しています。 s。以下に SubsystemBase
として紹介しました。以下に Proxy オブジェクト、 SubsystemProxy
を導入する必要があります。これは、 SubsystemBase
のインターフェースを実装し、リクエストをプロキシしているオブジェクト。 (この意味では、の特別な用途のアプリケーションに非常によく似ています。デコレータパターン)。各 Subsystem
はこれらのオブジェクトの1つを作成し、 shared_ptr
を介して保持し、 GetProxy()を介して要求されたときに返します。
GetSubsystem()
が呼び出されたときに親 System
オブジェクトによって呼び出されます。
System
が範囲外になると、その各 Subsystem
オブジェクトは破壊されます。デストラクタでは、 mProxy-&gt; Nullify()
を呼び出します。これにより、 Proxy オブジェクトが State 。これを行うには、 SubsystemBase
インターフェースを実装する Nullオブジェクト を指すように変更しますが、何もしません。
状態パターン を使用すると、特定の Subsystem
が存在するかどうかをクライアントアプリケーションが完全に無視できます。さらに、ポインターを確認したり、破棄されるべきインスタンスを保持したりする必要はありません。
他のヒント
これは、 weak_ptr
クラスを適切に使用することで実行できます。実際、あなたはすでに良い解決策を持っています。あなたは、「考え抜く」ことを期待できないことは正しい。クライアントプログラマーも、常に「ルール」に従うことを期待するべきではありません。あなたのAPIの(あなたがすでに知っていると確信しているように)。だから、あなたが本当にできる最善のことはダメージコントロールです。
GetSubsystem
への呼び出しで、 shared_ptr
ではなく weak_ptr
を返すことをお勧めします。これにより、クライアント開発者は、常にそれへの参照を主張せずにポインタ。
同様に、 pParentSystem
を boost :: weak_ptr&lt; System&gt;
にして、親 System
がまだ存在するかどうかを内部的に検出できるようにします NULL
のチェックと一緒に pParentSystem
で lock
を呼び出します(生のポインタではこれはわかりません)。
対応する System
オブジェクトが存在するかどうかを常に確認するために Subsystem
クラスを変更すると仮定すると、クライアントプログラマーが不可解な例外(キャッチする/適切に処理するためにクライアントプログラマーを信頼する必要がある)ではなく、エラーが発生する(制御する)意図したスコープ外のサブシステム
オブジェクト。
したがって、 main()
を使用した例では、物事はうまくいきません! Subsystem
のdtorでこれを処理する最も優雅な方法は、次のようにすることです。
class Subsystem
{
...
~Subsystem() {
boost::shared_ptr<System> my_system(pParentSystem.lock());
if (NULL != my_system.get()) { // only works if pParentSystem refers to a valid System object
// now you are guaranteed this will work, since a reference is held to the System object
my_system->LogMessage( "Destroying..." );
}
// Destroy this subsystem: deallocate memory, release resource, etc.
// when my_system goes out of scope, this may cause the associated System object to be destroyed as well (if it holds the last reference)
}
...
};
これが役立つことを願っています!
ここでは、システムがサブシステムを明確に所有しており、所有権を共有しても意味がありません。私は単純に生のポインタを返します。サブシステムがそのシステムよりも寿命が長い場合、それ自体がエラーになります。
あなたは最初のパラグラフの最初にいた。 RAIIに基づく設計(私のものや最もよく書かれたC ++コードなど)では、オブジェクトは排他的所有権ポインターによって保持される必要があります。 Boostではscoped_ptrになります。
では、なぜscoped_ptrを使用しなかったのですか。おそらく、ぶら下がり参照を防ぐためにweak_ptrの利点が必要だったからでしょう。しかし、weak_ptrをshared_ptrに向けることしかできません。したがって、本当に必要なのが単一所有権である場合、shared_ptrを便宜的に宣言する一般的な方法を採用しています。これは誤った宣言であり、あなたが言うように、正しい順序で呼び出されるデストラクタを危険にさらします。もちろん、所有権を一度も共有したことがなければ、それで済ますことができますが、共有されていないことを確認するために、常にすべてのコードをチェックする必要があります。
事態を悪化させるために、boost :: weak_ptrを使用するのは不便です(-&gt;演算子はありません)。したがって、プログラマは、受動的観測参照をshared_ptrとして誤って宣言することにより、この不便を回避します。もちろんこれは所有権を共有し、そのshared_ptrをnullに設定し忘れた場合、オブジェクトは破棄されず、意図したときにデストラクタが呼び出されません。
要するに、あなたはboostライブラリに助けられています-それは良いC ++プログラミング慣行を受け入れず、プログラマーにそれから利益を得るために偽の宣言をすることを強制します。純粋に共有所有権を必要とし、メモリや正しい順序で呼び出されるデストラクタを厳密に制御することに興味がない接着剤コードのスクリプトにのみ役立ちます。
私はあなたと同じ道を進んでいます。ダングリングポインターに対する保護はC ++で非常に必要ですが、ブーストライブラリは許容できるソリューションを提供しません。私はこの問題を解決しなければなりませんでした-私のソフトウェア部門は、C ++を安全にできるという保証を望んでいました。だから私は自分で転がした-それは非常に多くの仕事であり、で見つけることができます:
http://www.codeproject.com/KB/cpp/XONOR.aspx
これは、シングルスレッドの作業には完全に適切であり、スレッド間で共有されるポインターを受け入れるように更新しようとしています。その主な機能は、排他的所有オブジェクトのスマート(セルフゼロ化)パッシブオブザーバーをサポートすることです。
残念なことに、プログラマーはガベージコレクションに魅了され、「ワンサイズフィットオール」スマートポインターソリューションであり、所有権や受動的なオブザーバーについてもほとんど考えていません。その結果、彼らは自分が何をしているのかさえ知りません間違っていると文句を言わないでください。ブーストに対する異端はほとんど前代未聞です!
あなたに提案された解決策はとてつもなく複雑で、何の助けにもなりません。それらは、オブジェクトポインターが明確に宣言する必要のある明確な役割を持っていることを認識することに対する文化的な抵抗と、Boostが解決策でなければならないという盲信に起因する不条理の例です。
System :: GetSubsystemがサブシステムにrawポインター(スマートポインターよりも大きい)を返すことに問題はありません。クライアントはオブジェクトの構築に責任を負わないため、クライアントがクリーンアップに責任を負うという暗黙の契約はありません。また、内部参照であるため、サブシステムオブジェクトのライフタイムはシステムオブジェクトのライフタイムに依存していると仮定するのが妥当です。その後、ドキュメントを使用してこの暗黙の契約を強化する必要があります。
ポイントは、所有権を再割り当てまたは共有していないということです。なぜスマートポインターを使用するのですか?
ここでの本当の問題はあなたのデザインです。モデルには適切な設計原則が反映されていないため、適切なソリューションはありません。これが私が使用する便利な経験則です:
- オブジェクトが他のオブジェクトのコレクションを保持し、そのコレクションから任意のオブジェクトを返すことができる場合、そのオブジェクトをデザインから削除します。
あなたの例は不自然であることに気づきましたが、それはアンチパターンであり、私は多くの仕事をしています。 std :: vector&lt;を追加する
しませんか? APIのユーザーは System
の値は何ですか。 shared_ptr&lt; SubSystem&gt; &gt; SubSystem
のインターフェースを知っている必要があります(返却するため)ので、それらのホルダーを書くことは複雑さを増すだけです。少なくとも人々は std :: vector
へのインターフェースを知っており、 at()
または operator [ ]
は単なる平均です。
あなたの質問はオブジェクトの寿命を管理することですが、オブジェクトの配布を開始すると、他の人がそれらを生き続けることを許可することで寿命を制御できなくなり( shared_ptr
)、使用された場合にクラッシュするリスクがあります彼らが去った後(生のポインタ)。マルチスレッドのアプリではさらに悪いことです-誰があなたが別のスレッドに渡したオブジェクトをロックしますか?ブースト共有およびウィークポインターは、この方法で使用すると複雑なトラップになります。特に、経験の浅い開発者をつまずくのに十分なだけスレッドセーフであるためです。
ホルダーを作成する場合、ユーザーから複雑さを隠し、自分で管理できる負担を軽減する必要があります。例として、a)コマンドをサブシステムに送信する(たとえば、URI-/ system / subsystem / command?param = value)およびb)サブシステムおよびサブシステムコマンドを反復する(stlのようなイテレーターを使用)およびc)サブシステムを登録すると、実装の詳細のほとんどすべてをユーザーから隠すことができ、ライフタイム/順序/ロックの要件を内部的に実施できます。
反復可能/列挙可能なAPIは、どのような場合でもオブジェクトを公開するよりも非常に望ましいです-コマンド/登録は、テストケースまたは構成ファイルを生成するために簡単にシリアル化でき、対話形式で表示できます(たとえば、ツリーコントロールで、ダイアログを使用して、使用可能なアクション/パラメーターを照会して構成されます)。また、サブシステムクラスに行う必要のある内部変更からAPIユーザーを保護します。
アーロンズの回答のアドバイスに従わないよう警告します。 5つの異なる設計パターンを実装する必要があるこの単純な問題の解決策を設計することは、間違った問題が解決されていることを意味するだけです。また、デザインに関してマイヤーズ氏を引用する人は誰でも疲れています。
&quot;私は20年以上プロダクションソフトウェアを書いたことがなく、C ++でプロダクションソフトウェアを書いたことはありません。いや、これまでにない。さらに、私はC ++でプロダクションソフトウェアを記述しようとさえしなかったので、私は本物のC ++開発者ではないだけでなく、私も望んでいません。これをわずかに相殺するのは、大学院時代(1985年から1993年)にC ++で研究ソフトウェアを書いたという事実ですが、それでも小さな(数千行)単一の開発者がすぐに投げ捨てられるものでした。そして、十数年前にコンサルタントとして働き始めて以来、私のC ++プログラミングはおもちゃに限定されていました。これがどのように機能するかを見てみましょう&#8221; (または、時々、これが壊れるコンパイラの数を確認しましょう)プログラム、通常は単一のファイルに収まるプログラム&quot;。
彼の本は読む価値がないとは言いませんが、デザインや複雑さについて話す権限はありません。
あなたの例では、システムが vector&lt; shared_ptr&lt; Subsystem&gt;ではなく
。その両方がシンプルであり、あなたが持っている懸念を排除します。 GetSubsystemは代わりに参照を返します。 vector&lt; Subsystem&gt;
を保持しているとよいでしょう。 &gt;
スタックオブジェクトはインスタンス化された順序と逆の順序でリリースされるため、APIを使用する開発者がスマートポインターを管理しようとしていない限り、通常は問題になりません。防止できないものがいくつかありますが、実行時に警告を提供することが最善です。できればデバッグのみです。
あなたの例は私にとってCOMに非常によく似ています。shared_ptrを使用して返されるサブシステムの参照カウントがありますが、システムオブジェクト自体にはありません。
各サブシステムオブジェクトが作成時にシステムオブジェクトに対してaddrefを実行し、破棄時にリリースを実行した場合、システムオブジェクトが早期に破棄されるときに参照カウントが正しくない場合、少なくとも例外を表示できます。
weak_ptrを使用すると、代わりにメッセージを提供したり、間違った順序で解放されたときに爆発したりすることもできます。
問題の本質は循環参照です。システムはサブシステムを指し、サブシステムはシステムを指します。この種のデータ構造は、参照カウントでは簡単に処理できません。適切なガベージコレクションが必要です。エッジの1つに生のポインターを使用してループを中断しようとしています-これはより多くの複雑さを生むだけです。
少なくとも2つの良い解決策が提案されているので、以前のポスターをしのぐつもりはありません。 @Aaronのソリューションでは、サブシステムではなくシステムのプロキシを持つことができます-より複雑で意味のあるものに依存しています。