web-dev-qa-db-ja.com

std :: tr1 :: shared_ptrはどのように実装されていますか?

私は共有ポインタの使用を考えていましたが、自分で実装する方法を知っています-したくないので、std::tr1::shared_ptrを試してみて、いくつか質問があります...

参照カウントはどのように実装されていますか?二重にリンクされたリストを使用していますか? (ところで、私はすでにググっていますが、信頼できるものを見つけることができません。)

std::tr1::shared_ptrを使用する際の落とし穴はありますか?

35
purepureluck

shared_ptrは、参照カウンターと、初期化時に指定されたオブジェクトのタイプによって推定される削除機能の伝達を管理する必要があります。

shared_ptrクラスは通常、2つのメンバーをホストします。T*operator->によって返され、operator*で逆参照されます)とaux*で、auxは以下を含む内部抽象クラスです。

  • カウンター(コピー割り当て/破棄時に増分/減分)
  • インクリメント/デクリメントをアトミックにするために必要なもの(特定のプラットフォームアトミックINC/DECが使用可能な場合は不要)
  • 抽象virtual destroy()=0;
  • 仮想デストラクタ。

そのようなauxクラス(実際の名前は実装によって異なります)は、テンプレート化されたクラス(明示的なコンストラクターによって指定された型でパラメーター化され、たとえばUから派生したT)のファミリーによって派生します。

  • オブジェクトへのポインター(T*と同じですが、実際のタイプ:これは、Tが派生階層で複数のUを持つTのベースであるすべてのケースを適切に管理するために必要です)
  • 明示的なコンストラクターへの削除ポリシーとして指定されたdeletorオブジェクトのコピー(または、削除deletorを実行するだけのデフォルトのp、ここでpは上記のU*です)
  • destroyメソッドのオーバーライド。削除機能を呼び出します。

簡略化したスケッチは次のようになります。

template<class T>
class shared_ptr
{
    struct aux
    {
        unsigned count;

        aux() :count(1) {}
        virtual void destroy()=0;
        virtual ~aux() {} //must be polymorphic
    };

    template<class U, class Deleter>
    struct auximpl: public aux
    {
        U* p;
        Deleter d;

        auximpl(U* pu, Deleter x) :p(pu), d(x) {}
        virtual void destroy() { d(p); } 
    };

    template<class U>
    struct default_deleter
    {
        void operator()(U* p) const { delete p; }
    };

    aux* pa;
    T* pt;

    void inc() { if(pa) interlocked_inc(pa->count); }

    void dec() 
    { 
        if(pa && !interlocked_dec(pa->count)) 
        {  pa->destroy(); delete pa; }
    }

public:

    shared_ptr() :pa(), pt() {}

    template<class U, class Deleter>
    shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}

    template<class U>
    explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}

    shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }

    template<class U>
    shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }

    ~shared_ptr() { dec(); }

    shared_ptr& operator=(const shared_ptr& s)
    {
        if(this!=&s)
        {
            dec();
            pa = s.pa; pt=s.pt;
            inc();
        }        
        return *this;
    }

    T* operator->() const { return pt; }
    T& operator*() const { return *pt; }
};

weak_ptrの相互運用性が必要な場合は、auxに2番目のカウンター(weak_count)が必要です(weak_ptrによってインクリメント/デクリメントされます)。また、delete paは、両方のカウンターがゼロに達したときにのみ発生する必要があります。

55

参照カウントはどのように実装されますか?

ポリシーベースのクラス設計 を使用して、スマートポインターの実装を分解できます。1、に:

  • ストレージポリシー

  • 所有権ポリシー

  • 変換ポリシー

  • チェックポリシー

テンプレートパラメータとして含まれています。一般的な所有権戦略には、ディープコピー、参照カウント、参照リンク、破壊的コピーが含まれます。

参照カウントは、(所有している)スマートポインターの数を追跡します2)同じオブジェクト。数値がゼロになると、指示先オブジェクトが削除されます。実際のカウンターは次のようになります。

  1. スマートポインターオブジェクト間で共有され、各スマートポインターは参照カウンターへのポインターを保持します。

enter image description here

  1. 指示先オブジェクトに間接的なレベルを追加する追加の構造にのみ含まれています。ここで、各スマートポインターにカウンターを保持することによるスペースオーバーヘッドは、より遅いアクセス速度と交換されます。

enter image description here

  1. Pointeeオブジェクト自体に含まれている、侵入参照カウント。欠点は、オブジェクトを数える機能を備えたアプリオリに構築する必要があることです。

    enter image description here

  2. 最後に、あなたの質問のメソッド、二重にリンクされたリストを使用した参照カウントは、参照リンクと呼ばれ、それは次のとおりです。

... [1] 1つの指示先オブジェクトを指すスマートポインターオブジェクトの実際の数は実際には必要ないという観察に依存しています。そのカウントがゼロになるときを検出する必要があるだけです。これは、「所有権リスト」を保持するという考えにつながります。

enter image description here

参照カウントよりも参照リンクの利点は、前者が余分なフリーストアを使用しないことです。これにより、信頼性が向上します。参照リンクされたスマートポインターの作成は失敗しません。不利な点は、参照リンクが簿記のために多くのメモリを必要とすることです(3つのポインターと1つのポインターと1つの整数のみ)。また、参照カウントは少し高速である必要があります。スマートポインタをコピーする場合、必要なのは間接と増分だけです。リスト管理はもう少し複雑です。結論として、無料のストアが不足している場合にのみ、参照リンクを使用する必要があります。それ以外の場合は、参照カウントを優先します。

2番目の質問について:

(_std::shared_ptr_)は二重にリンクされたリストを使用していますか?

C++標準で見つけることができたのは次のとおりです。

20.7.2.2.6 shared_ptrの作成
...
7。 [注:これらの関数は通常、sizeof(T)より多くのメモリを割り当てて、参照カウントなどの内部簿記構造を考慮に入れます。 —エンドノート]

私の意見では、実際のカウントが含まれていないため、二重にリンクされたリストは除外されます。

3番目の質問:

_std::shared_ptr_を使用する際の落とし穴はありますか?

カウントまたはリンクのいずれかの参照管理は、循環参照として知られるリソースリークの犠牲者です。オブジェクトBへのスマートポインタを保持するオブジェクトAがあるとします。また、オブジェクトBはAへのスマートポインタを保持します。これら2つのオブジェクトは循環参照を形成します。それらのいずれも使用しなくなったとしても、お互いを使用します。参照管理戦略はこのような循環参照を検出できず、2つのオブジェクトは永久に割り当てられたままになります。

_shared_ptr_の実装は参照カウントを使用するため、循環参照が潜在的に問題になります。循環の_shared_ptr_チェーンは、参照の1つが_weak_ptr_になるようにコードを変更することで解除できます。これは、共有ポインターとウィークポインターの間に値を割り当てることによって行われますが、ウィークポインターは参照カウントに影響しません。オブジェクトを指す唯一のポインターが弱い場合、オブジェクトは破棄されます。


1.ポリシーとして定式化されている場合、複数の実装を持つ各設計機能。

2. newで割り当てられたオブジェクトを指すポインターと同様のスマートポインターは、そのオブジェクトを指すだけでなく、そのオブジェクトの破棄と、それが占有するメモリの解放も行います。

3.他の生のポインターが使用されていないか、ポインターをポイントしていない場合、問題は発生しません。

[1]現代のC++デザイン:一般的なプログラミングとデザインパターンを適用。 Andrei Alexandrescu、2001年2月1日

29
Ziezi

すべての詳細を確認したい場合は、boost shared_ptrの実装を確認できます。

https://github.com/boostorg/smart_ptr

参照カウントは通常、カウンターとプラットフォーム固有のアトミックインクリメント/デクリメント命令またはミューテックスを使用した明示的なロックで実装されているようです( detail名前空間atomic_count_*.hppファイルを参照)。

4
sth

std::tr1::shared_ptrを使用する際の落とし穴はありますか?

はい、共有メモリポインタにサイクルを作成した場合、スマートポインタによって管理されているメモリは、ポインタへの参照が残っているため、最後のポインタがスコープから外れたときにリサイクルされません(つまり、サイクルによって参照カウントが発生します)ゼロに下がらないように)。

例えば:

struct A
{
    std::shared_ptr<A> ptr;
};

std::shared_ptr<A> shrd_ptr_1 = std::make_shared(A());
std::shared_ptr<B> shrd_ptr_2 = std::make_shared(A());
shrd_ptr_1->ptr = shrd_ptr_2;
shrd_ptr_2->ptr = shrd_ptr_1;

現在、shrd_ptr_1shrd_ptr_2がスコープ外になったとしても、それぞれのptrメンバーが互いにポイントしているため、それらが管理しているメモリは解放されません。これはそのようなメモリサイクルの非常に単純な例ですが、これらの種類のポインタを規律なしで使用すると、はるかに悪質で追跡が困難な方法で発生する可能性があります。たとえば、各nextポインタがstd::shared_ptrである循環リンクリストを実装しようとすると、あまり注意しないと問題が発生する可能性がある場所を確認できます。

3
Jason