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の内部構造がさらに変更される可能性があります。
秘Theは 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)
shared_ptr
が別のコピーからコピー(またはデフォルトで作成)され、削除プログラムが渡されるため、shared_ptr<T>
から shared_ptr<U>
呼び出すデストラクタに関する情報もdeleter
で渡されます。
shared_ptr<T>
logically [*]には(少なくとも)2つの関連データメンバーがあります。
shared_ptr<Test>
の削除機能は、作成方法に応じて、Test
の通常の機能であり、ポインターをTest*
とdelete
sに変換します。
shared_ptr<Test>
をshared_ptr<void>
のベクトルにプッシュすると、最初の1つがvoid*
に変換されますが、これらのbothがコピーされます。
そのため、最後の参照を使用してベクター要素が破棄されると、ポインターを削除子に渡し、正しく削除します。
shared_ptr
は単なる関数ではなく削除子functorを取ることができるため、実際にはこれよりも少し複雑です。したがって、関数ごとではなくオブジェクトごとのデータを保存することもできます。ポインター。ただし、この場合、そのような余分なデータはありません。ポインターを削除する必要がある型をキャプチャするテンプレートパラメーターを使用して、テンプレート関数のインスタンス化へのポインターを格納するだけで十分です。
[*]論理的にはそれらにアクセスできるという意味で-それらはshared_ptr自体のメンバーではなく、それが指し示す管理ノードではないかもしれません。
型消去を使用しているため機能します。
基本的に、shared_ptr
を作成すると、1つの追加の引数(必要に応じて実際に指定できます)が渡されます。これは、削除ファンクターです。
このデフォルトのファンクタは、shared_ptr
で使用する型へのポインタを引数として受け取り、ここでvoid
を使用して、ここで使用した静的型に適切にキャストし、test
を使用し、デストラクタを呼び出しますこのオブジェクト上。
十分に進歩した科学は魔法のように感じますよね?
コンストラクター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>
オブジェクトを受け入れるには、U*
が暗黙的にT*
に変換可能である必要があり、どのポインターもT=void
に暗黙的に変換できるため、void*
に暗黙的に変換できる必要があります。削除者については無効になるものは何も言われていないため、実際には仕様がこれが正しく機能することを義務付けています。
技術的にはIIRC a shared_ptr<T>
は、参照カウンターを含む隠しオブジェクトへのポインターと実際のオブジェクトへのポインターを保持します。この隠された構造に削除プログラムを保存することにより、shared_ptr<T>
を通常のポインターと同じ大きさに保ちながら、この一見魔法の機能を動作させることができます(ただし、ポインターの逆参照には二重間接参照が必要です)
shared_ptr -> hidden_refcounted_object -> real_object
Test*
はvoid*
に暗黙的に変換可能であるため、shared_ptr<Test>
はメモリからshared_ptr<void>
に暗黙的に変換可能です。これは、shared_ptr
がコンパイル時ではなく実行時の破壊を制御するように設計されているため、内部的に継承を使用して適切なデストラクタを呼び出し時に割り当てるためです。
ユーザーが理解できるshared_ptrの非常に単純な実装を使用して、この質問に答えます(2年後)。
まず、いくつかのサイドクラス、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クラス自体は、演算子のオーバーロード(->、*など)のタイプセーフな使用を管理します。
したがって、空にするshared_ptrがありますが、その下では、newに渡した型のポインターを管理しています。 shared_ptrに入れる前にポインターをvoid *に変換すると、checked_deleteでのコンパイルに失敗するため、実際にそこでも安全であることに注意してください。