[c++] スマートポインタと不可避的な非決定性に関する質問



3 Answers

これは、 weak_ptrクラスを適切に使用することで実現できます。 実際、あなたはすでに良い解決策を取っていることに非常に近いです。 あなたは、あなたのクライアントプログラマーを「考える」ことが期待できず、あなたがAPIの「ルール」に従うことも期待してはいけません(あなたがすでに気づいていると確信しています)。 だから、あなたが本当にできることは、ダメージコントロールです。

GetSubsystemshared_ptrではなくweak_ptr返すように呼び出すことを推奨します。これにより、クライアント開発者は、ポインタの妥当性をいつでも参照することなくテストできるようになります。

同様に、 pParentSystem boost::weak_ptr<System>すると、親SystempParentSystemlockする呼び出しとNULLチェック(未処理のポインタはこれを知らせない)を介して内部的に存在するかどうかを内部的に検出できる。

Subsystemクラスを変更して、対応する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)
  }
...
};

私はこれが助けて欲しい!

Question

私は過去2年間私のプロジェクトでスマートポインタ(boost :: shared_ptrを厳密に使用)を広範囲に使用してきました。 私は彼らの利益を理解して感謝し、私はそれらをたくさん好むのです。 しかし、私がそれらを使用するほど、C ++の決定論的な振る舞いがメモリ管理やRAIIに関してはプログラミング言語で好きであるように見えなくなります。 スマートポインタはメモリ管理のプロセスを簡素化し、他のものの中で自動ガベージコレクションを提供しますが、一般的なスマートポインタでの自動ガベージコレクションを使用すると、初期化の順序である程度の不確定性が特に発生します。 この非確定主義はプログラマからのコントロールを取り去り、最近実現するように、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を正しく使いやすく使いにくくすることをお勧めします。 だから私はあなたたちに頼んでいる。 どう思いますか? この問題をどのように緩和すべきですか? あなたはどのようにそのようなシステムを設計しますか?

事前に感謝、ジョシュ




あなたは最初の段落の最初のところにいました。 RAIIをベースにしたデザイン(鉱山やよく書かれたC ++コードのような)では、オブジェクトが排他的所有権ポインタによって保持されている必要があります。 ブーストではscoped_ptrになります。

なぜscoped_ptrを使わなかったのですか? あなたがweak_ptrの利点を迷惑な参照から守ることを望んでいたからですが、weak_ptrをshared_ptrで指すことができるからです。 したがって、あなたが本当に望んでいたものが単一の所有権であったときに、shared_ptrを宣言するという一般的な方法を採用しました。 これは誤った宣言であり、あなたが言うように、正しい順序で呼び出されるデストラクタを妥協します。 もちろん所有権を共有していない場合は、その所有権を失うことになりますが、共有されていないことを常に確認するためには、すべてのコードを常にチェックする必要があります。

問題を悪化させるために、boost :: weak_ptrは使用するのに不便です( - >演算子はありません)ので、プログラマはshared_ptrという受動的な参照を誤って宣言することによってこの不都合を回避します。 これはもちろんオーナーシップを共有しています。そのshared_ptrをnullにすることを忘れた場合、あなたのオブジェクトは破棄されません。

要するに、あなたはブーストライブラリによって軸化されています。それは良いC ++プログラミングの慣行を受け入れず、プログラマが虚偽の宣言をしてそれからいくつかの恩恵を受けるよう強制します。 これは、共有所有権を真に必要とするグルーコードをスクリプティングする場合にのみ有用であり、メモリまたはデストラクタを厳密に制御して正しい順序で呼び出すことには関心がありません。

私はあなたと同じ道を歩いている。 ぶら下がっているポインタに対する保護はC ++では必要不可欠ですが、ブーストライブラリは許容できる解決策を提供しません。 私はこの問題を解決しなければなりませんでした。私のソフトウェア部門は、C ++を安全にすることができるという保証を求めました。 だから私は自分自身を巻き込んだ - それはかなりの仕事であり、

http://www.codeproject.com/KB/cpp/XONOR.aspx

シングルスレッドの作業にはまったく適しています。スレッド間で共有されるポインタを使用するように更新しようとしています。 その主な特徴は、独占所有オブジェクトのスマート(自己ゼロ化)パッシブオブザーバをサポートすることです。

残念なことに、プログラマーはガーベジコレクションによって誘惑されており、「1つのサイズはすべてのスマートポインタソリューションに適合しており、主にオーナーシップと受動的なオブザーバーについて考えることさえしていません。結果として、彼らは自分がしていることが間違っていて、不平を言う。 ブーストに対する異端はほとんど聞いていません!

あなたに示唆されている解決策は、ばかげて複雑で、役に立たない。 それらは、オブジェクトポインタが正しく宣言されなければならない別個の役割を持ち、ブーストが解決策でなければならないという盲目的な信念を持つことを認識することに対する文化的な躊躇から生じる不条理の例です。




スタックオブジェクトは、インスタンス化された順序とは逆の順序でリリースされるため、APIを使用する開発者がスマートポインタを管理しようとしない限り、通常は問題にはなりません。 あなたが防ぐことができないようなものがいくつかありますが、実行時に警告を出すことができます。

あなたの例は私にとってCOMと非常によく似ていますが、shared_ptrを使って返されるサブシステムの参照カウントはありますが、システムオブジェクト自体にはその値がありません。

サブシステム・オブジェクトのそれぞれが作成時にシステム・オブジェクトにaddrefを実行し、破壊時にリリースが発生した場合は、システム・オブジェクトが早期に破棄されたときに参照カウントが正しくない場合、少なくとも例外を表示できます。

weak_ptrを使用すると、物事が間違った順序で解放されたときに爆発するのを防ぐことができます。




ここの本当の問題はあなたのデザインです。 モデルには良い設計原則が反映されていないため、いい解はありません。 私が使用する便利なルールは次のとおりです。

  • オブジェクトが他のオブジェクトのコレクションを保持し、そのコレクションから任意のオブジェクトを返すことができる場合は、そのオブジェクトをデザインから削除します

私は、あなたの例が考案されていることを認識していますが、その反パターンは私が仕事で多く見ます。 System std::vector< shared_ptr<SubSystem> >という値を追加するのはどのような値ですか? あなたのAPIのユーザはSubSystemのインタフェースを知っている必要があります(あなたがそれらを返すので)、そのための所有者を書くことは複雑さを増すだけです。 少なくとも人々はstd::vectorへのインタフェースを知っています。 at()operator[]上のGetSubsystem()が覚えていることを強制するのは単なる意味です。

あなたの質問はオブジェクトのライフタイムを管理することですが、オブジェクトを渡し始めると、他の人にそれらを生かしておくこと( shared_ptr )や、離れた後に使用された場合(rawポインタ)のリスククラッシュなどがあります。 マルチスレッドのアプリケーションでは、さらに悪いことに、あなたは別のスレッドに渡しているオブジェクトをロックしていますか? 共用ポインタと弱ポインタ​​は、この方法で使用すると複雑なものになります。特に、経験の浅い開発者には十分にスレッドセーフであるためです。

所有者を作成する場合は、ユーザーから複雑さを隠し、自分で管理できる負担を軽減する必要があります。 例えば、a)サブシステムへのコマンド送信(例えば、URI- /システム/サブシステム/コマンド-param =値)とb)サブシステムとサブシステムコマンドの反復(stl-likeイテレータを介して)、場合によってはc)ユーザーの実装の詳細をほとんどすべて隠すことができ、内部でライフタイム/注文/ロックの要件を満たすことができます。

どのような場合でもオブジェクトを公開するには反復可能/列挙可能なAPIが非常に好ましい - コマンド/登録はテストケースや設定ファイルを生成するために簡単にシリアライズすることができ、対話的に表示することもできる(例えば、ツリーコントロールやクエリ利用可能なアクション/パラメータ)。 また、サブシステムクラスに加える必要のある内部的な変更からAPIユーザーを保護することもできます。

私はAaronsの答えの助言に従わないように注意します。 問題への解決策の設計この5つの異なる設計パターンを実装する必要があるこのシンプルな方法は、間違った問題が解決されていることを意味するだけです。 私はまた、彼自身の入場によって、設計に関してマイヤーズ氏を引用する人には疲れている。

「私は20年以上前からプロダクションソフトウェアを書いていませんでしたが、C ++でプロダクションソフトウェアを書いたことはありませんでした。 C ++の開発者でも、私は大したことではありません。これをわずかに相殺することは、私が大学院時代(1985-1993)にC + +で研究ソフトウェアを作成したという事実ですが、それは小さかった(数千行)数十年前にコンサルタントとして勤めて以来、私のC ++プログラミングはおもちゃに限定されていました。「これがどういう仕組みか見てみましょう」(あるいは、「いくつのコンパイラがこれは壊れます ")プログラム、通常は1つのファイルに収まるプログラムです"。

彼の本は読める価値がないと言っているわけではありませんが、設計や複雑さについて話す権限はありません。




Related