std :: shared_ptr <void>が機能する理由


129

シャットダウン時にstd :: shared_ptrを使用して任意のクリーンアップを実行するコードを見つけました。最初はこのコードは機能しないと思っていましたが、次のことを試しました。

#include <memory>
#include <iostream>
#include <vector>

class test {
public:
  test() {
    std::cout << "Test created" << std::endl;
  }
  ~test() {
    std::cout << "Test destroyed" << std::endl;
  }
};

int main() {
  std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>" 
            << std::endl;
  std::vector<std::shared_ptr<void>> v;
  {
    std::cout << "Creating test" << std::endl;
    v.push_back( std::shared_ptr<test>( new test() ) );
    std::cout << "Leaving scope" << std::endl;
  }
  std::cout << "Leaving main" << std::endl;
  return 0;
}

このプログラムは出力を提供します:

At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed

これが機能する理由についていくつかのアイデアがあります。これは、G ++に実装されているstd :: shared_ptrsの内部に関係しています。これらのオブジェクトは内部ポインターをカウンターと一緒にラップするので、キャスト元からキャスト先std::shared_ptr<test>へのキャストstd::shared_ptr<void>はおそらくデストラクタの呼び出しを妨げていません。この仮定は正しいですか?

そしてもちろん、はるかに重要な質問:これは標準で動作することが保証されていますか、それともstd :: shared_ptrの内部にさらに変更が加えられる可能性があります。他の実装は実際にこのコードを壊しますか?


2
代わりに何が起こると思いましたか?
軌道上での軽さのレース

1
キャストはありません-shared_ptr <test>からshared_ptr <void>への変換です。
Alan Stokes、

参考までに:MSDNのstd :: shared_ptrに関する記事へのリンク:msdn.microsoft.com/en-us/library/bb982026.aspxこれはGCCのドキュメントです:gcc.gnu.org/onlinedocs/libstdc++
yasouser

回答:


98

トリックはstd::shared_ptr型消去を実行することです。基本的に、新規shared_ptrが作成されると、deleter関数が内部的に格納されます(コンストラクターに引数として指定できますが、存在しない場合はデフォルトでを呼び出しますdelete)。ときにshared_ptr破壊され、その保存された関数を呼び出し、それが呼び出されますdeleter

std :: functionで単純化され、すべての参照カウントやその他の問題を回避している型消去の簡単なスケッチは、次のとおりです。

template <typename T>
void delete_deleter( void * p ) {
   delete static_cast<T*>(p);
}

template <typename T>
class my_unique_ptr {
  std::function< void (void*) > deleter;
  T * p;
  template <typename U>
  my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> ) 
     : p(p), deleter(deleter) 
  {}
  ~my_unique_ptr() {
     deleter( p );   
  }
};

int main() {
   my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double>
}
// ~my_unique_ptr calls delete_deleter<double>(p)

a shared_ptrが別のaからコピーされた(またはデフォルトで作成された)場合、削除子が渡されるため、a shared_ptr<T>からaを作成しshared_ptr<U>た場合、どのデストラクタを呼び出すかに関する情報もで渡されますdeleter


誤植があるようです:my_shared。私はそれを修正しますが、まだ編集する権限がありません。
Alexey Kukanov、

@Alexey Kukanov、@ Dennis Zickefoose:編集してくれてありがとう。
デビッド・ロドリゲス-11

2
@ user102008 'std :: function'は必要ありませんが、少し柔軟性があります(おそらくここではまったく問題ではありません)が、 'delete_deleter <T>'を関数ポインタ 'void(void *)'あなたはそこで型消去を実行しています:Tは格納されたポインタ型から消えています。
DavidRodríguez-

1
この動作はC ++標準で保証されていますよね?クラスの1つで型消去が必要でstd::shared_ptr<void>、特定の基本クラスから継承できるように、役に立たないラッパークラスを宣言しないようにします。
バイオレットキリン14

1
@AngelusMortis:正確な削除機能はのタイプの一部ではありませんmy_unique_ptrmainテンプレート内でdouble正しいインスタンスを使用してインスタンス化されている場合、削除者が選択されますが、これはのタイプの一部ではなくmy_unique_ptr、オブジェクトから取得できません。削除者のタイプはオブジェクトから消去され、関数がmy_unique_ptr(たとえば、右辺値参照によって)を受け取った場合、その関数は削除者が何であるかを知りません。
デビッドロドリゲス-2016年

35

shared_ptr<T> logically [*]には、(少なくとも)2つの関連データメンバーがあります。

  • 管理されているオブジェクトへのポインタ
  • それを破壊するために使用される削除関数へのポインター。

の削除関数はshared_ptr<Test>、作成方法を考えると、の通常の関数でTestあり、ポインタを変換しTest*deletesにします。

shared_ptr<Test>をのベクターにプッシュすると、最初のベクターはに変換されますがshared_ptr<void>両方がコピーされvoid*ます。

そのため、最後の参照でベクター要素が破棄されると、それはポインターを削除者に渡し、削除者はそれを正しく破棄します。

単なる関数ではなくshared_ptr削除関数を使用できるため、実際にはこれよりも少し複雑です。そのため、関数ポインタだけでなくオブジェクトごとのデータを格納することもできます。ただし、この場合、そのような余分なデータはありません。テンプレート関数のインスタンス化へのポインターを格納し、ポインターを削除する必要がある型をキャプチャーするテンプレートパラメーターで十分です。

[*]論理的にはそれらにアクセスできるという意味で-それらはshared_ptr自体のメンバーではなく、それが指すいくつかの管理ノードではない可能性があります。


2
削除機能/ファンクターが他のshared_ptrインスタンスにコピーされることを言及するための+1-他の回答で欠落した情報の一部。
Alexey Kukanov

これは、shared_ptrsを使用するときに仮想ベースデストラクタが不要であることを意味しますか?
ronag

@ronagはい。ただし、少なくとも他の仮想メンバーがいる場合は、デストラクタを仮想化することをお勧めします。(誤って一度忘れてしまうという痛みは、考えられる利益を上回ります。)
アラン・ストークス

はい、同意します。それ以外は興味深い。型消去については、この「機能」を考慮していないことを知っていました。
ロナグ

2
@ronag:shared_ptr適切な型で直接を作成する場合、またはを使用する場合、仮想デストラクタは必要ありませんmake_shared。ただし、オブジェクトがそうでないかぎり、ポインタの型は構築からshared_ptrbase *p = new derived; shared_ptr<base> sp(p);に格納されるまでポインタの型が変わる可能性があるため、仮想デストラクタが必要になるため、それでも良い考えです。このパターンは、例えば、ファクトリー・パターンと共通にすることができます。shared_ptrbasederived
デビッドロドリゲス-11

10

型消去を使用しているため機能します。

基本的に、を構築するとshared_ptr、1つの追加の引数(必要に応じて実際に提供できる)を渡します。これは、削除機能です。

このデフォルトファンクタは、で使用する型へのポインタを引数として受け入れますshared_ptr。したがって、voidここでは、ここで使用した静的型に適切にキャストしtest、このオブジェクトのデストラクタを呼び出します。

十分に進歩した科学は魔法のように感じますね。


5

コンストラクタは、shared_ptr<T>(Y *p)実際に呼び出しているように見えるshared_ptr<T>(Y *p, D d)場所dオブジェクトの自動生成デリータです。

これが発生すると、オブジェクトのタイプがYわかるため、このshared_ptrオブジェクトの削除者は呼び出すデストラクタを認識し、ポインタがのベクトルに格納されているときにこの情報が失われることはありませんshared_ptr<void>

実際、仕様では、受信shared_ptr<T>オブジェクトがオブジェクトを受け入れるためにはshared_ptr<U>trueであるU*必要があり、暗黙的にaに変換可能である必要があります。ポインタは暗黙的に変換できるためT*、これは確かに当てはまります。無効になる削除プログラムについては何も述べられていないため、実際には仕様がこれを正しく機能させることを義務付けています。T=voidvoid*

技術的にはIIRC a shared_ptr<T>は、参照カウンターを含む隠しオブジェクトへのポインターと実際のオブジェクトへのポインターを保持します。この非表示の構造に削除機能を格納することによりshared_ptr<T>、通常のポインタと同じ大きさを保ちながら、明らかにこの魔法の機能を機能させることができます(ただし、ポインタの逆参照には二重の間接参照が必要です)

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*暗黙的に変換されるvoid*ので、shared_ptr<Test>に暗黙的に変換されshared_ptr<void>、メモリから、。これshared_ptrは、コンパイル時ではなく実行時に破棄を制御するように設計されているため機能します。割り当て時に、適切にデストラクタを呼び出すために内部的に継承を使用します。


詳細を説明できますか?私は今、同様の質問を投稿しました。あなたが助けてくれるといいのですが!
ブルース

3

この質問(2年後)には、ユーザーが理解できるshared_ptrの非常に単純な実装を使用して回答します。

最初に、いくつかのサイドクラス、shared_ptr_base、sp_counted_base sp_counted_impl、checked_deleterに行きます。これらの最後はテンプレートです。

class sp_counted_base
{
 public:
    sp_counted_base() : refCount( 1 )
    {
    }

    virtual ~sp_deleter_base() {};
    virtual void destruct() = 0;

    void incref(); // increases reference count
    void decref(); // decreases refCount atomically and calls destruct if it hits zero

 private:
    long refCount; // in a real implementation use an atomic int
};

template< typename T > class sp_counted_impl : public sp_counted_base
{
 public:
   typedef function< void( T* ) > func_type;
    void destruct() 
    { 
       func(ptr); // or is it (*func)(ptr); ?
       delete this; // self-destructs after destroying its pointer
    }
   template< typename F >
   sp_counted_impl( T* t, F f ) :
       ptr( t ), func( f )

 private:

   T* ptr; 
   func_type func;
};

template< typename T > struct checked_deleter
{
  public:
    template< typename T > operator()( T* t )
    {
       size_t z = sizeof( T );
       delete t;
   }
};

class shared_ptr_base
{
private:
     sp_counted_base * counter;

protected:
     shared_ptr_base() : counter( 0 ) {}

     explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {}

     ~shared_ptr_base()
     {
        if( counter )
          counter->decref();
     }

     shared_ptr_base( shared_ptr_base const& other )
         : counter( other.counter )
     {
        if( counter )
            counter->addref();
     }

     shared_ptr_base& operator=( shared_ptr_base& const other )
     {
         shared_ptr_base temp( other );
         std::swap( counter, temp.counter );
     }

     // other methods such as reset
};

次に、新しく作成されたものへのポインタを返すmake_sp_counted_implと呼ばれる2つの「フリー」関数を作成します。

template< typename T, typename F >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func )
{
    try
    {
       return new sp_counted_impl( ptr, func );
    }
    catch( ... ) // in case the new above fails
    {
        func( ptr ); // we have to clean up the pointer now and rethrow
        throw;
    }
}

template< typename T > 
sp_counted_impl<T> * make_sp_counted_impl( T* ptr )
{
     return make_sp_counted_impl( ptr, checked_deleter<T>() );
}

わかりました、これらの2つの関数は、テンプレート関数を介してshared_ptrを作成するときに次に何が起こるかに関して重要です。

template< typename T >
class shared_ptr : public shared_ptr_base
{

 public:
   template < typename U >
   explicit shared_ptr( U * ptr ) :
         shared_ptr_base( make_sp_counted_impl( ptr ) )
   {
   }

  // implement the rest of shared_ptr, e.g. operator*, operator->
};

TがvoidでUが「テスト」クラスである場合、上記の処理に注意してください。Tへのポインタではなく、Uへのポインタでmake_sp_counted_impl()を呼び出します。破棄の管理はすべてここで行われます。shared_ptr_baseクラスは、コピーや割り当てなどに関する参照カウントを管理します。shared_ptrクラス自体は、演算子オーバーロード(->、*など)のタイプセーフな使用を管理します。

したがって、voidへのshared_ptrがありますが、その下では、newに渡した型のポインタを管理しています。ポインタをvoid *に変換してからshared_ptrに入れると、checked_deleteでのコンパイルに失敗するため、実際に安全です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.