【C++】弱参照クラスを自作する
C++11から標準ライブラリにstd::weak_ptr
が入りました。std::weak_ptr
を使うことで弱参照を行えるようになるのですが、ちょっと使い勝手が悪いので弱参照クラスを自作してみました。
ちなみに、弱参照の良さについてはこちら↓で紹介しています。
はじめに
弱参照という言葉の定義
弱参照は「ガベージコレクションにてオブジェクトの延命を行わない参照」のことですが、一般的な仕様として「オブジェクトの削除を検知できる機能」も持っています。この記事ではそこに注目し、オブジェクトの削除を検知できる参照という意味で弱参照という言葉を使用しています。
なので、この記事で作成する弱参照クラスはガベージコレクションとは関係ありません。他の名称を付けるなら、ダングリングポインタにならない安全なポインタという意味で「SafePointer」とかでしょうか。または、自動で無効化されるということで「AutoNullPointer」とか…?まぁ、とりあえずこの記事中では弱参照として「WeakPtr」という名前にします。
std::weak_ptrを使わない理由
弱参照といえば、C++11から標準ライブラリにstd::weak_ptr
が入りましたが、以下のような仕様になっており少し使い勝手が悪いです。
std::shared_ptr
からしかstd::weak_ptr
を作ることができないので、利用できる場面が限られるstd::weak_ptr
から直接参照先にアクセスできない(std::shared_ptr
を経由する必要がある)
std::weak_ptr
はstd::shared_ptr
からしか作ることができないので、自動変数やメンバ変数から作ることができません。これはだいぶ不便です。
紹介するコードの注意点
- C++11以降が必須です
- 紹介するコードは自由に使ってもらって構いませんが、あまりちゃんと動作テストをしていないので参考程度にすることをおすすめします
自作してみた弱参照クラスのソースコード
// WeakPtr.h #include <assert.h> #include <type_traits> class PtrInfo { public: void* m_Ptr = nullptr; int m_RefCount = 0; PtrInfo( void* ptr ) : m_Ptr( ptr ){} void Inc( void ){ ++m_RefCount; } static void Dec( PtrInfo*& p_info ) { if (!p_info){ return; } --p_info->m_RefCount; if ( p_info->m_RefCount == 0 ) { delete p_info; p_info = nullptr; } } bool IsNull( void ) const { return m_Ptr == nullptr; } template<class T> T* GetPtr( void ) const { return reinterpret_cast<T*>(m_Ptr); } int GetRefCount( void ) const { return m_RefCount; } }; template<class T> class WeakPtrController; #define nonvirtual template<class T> class WeakPtr { friend class WeakPtrController<T>; PtrInfo* m_pPtrInfo = nullptr; public: nonvirtual ~WeakPtr() { PtrInfo::Dec( m_pPtrInfo ); } WeakPtr() { } explicit WeakPtr( PtrInfo* p_ptrInfo ) : m_pPtrInfo( p_ptrInfo ) { if ( m_pPtrInfo ) { m_pPtrInfo->Inc(); } } WeakPtr( const WeakPtr& other ) : m_pPtrInfo( other.m_pPtrInfo ) { if ( m_pPtrInfo ) { m_pPtrInfo->Inc(); } } WeakPtr& operator=( const WeakPtr& other ) { PtrInfo::Dec( m_pPtrInfo ); m_pPtrInfo = other.m_pPtrInfo; if ( m_pPtrInfo ) { m_pPtrInfo->Inc(); } return *this; } void Clear( void ) { PtrInfo::Dec( m_pPtrInfo ); m_pPtrInfo = nullptr; } template<class U> WeakPtr<U> GetUpcasted( void ) { static_assert( std::is_base_of<U,T>::value, "" ); // U が T の基底クラスである必要がある return WeakPtr<U>( m_pPtrInfo ); } template<class U> WeakPtr<U> GetDowncasted( void ) { static_assert( std::is_base_of<T,U>::value, "" ); // U は T の派生クラスである必要がある if ( dynamic_cast<U*>( GetPtr() ) ) // ポインタが実際に U型 インスタンスであるかどうかをチェック { return WeakPtr<U>( m_pPtrInfo ); } return WeakPtr<U>( nullptr ); // ダウンキャストに失敗したらNULLポインタ(のWeakPtr)を返す } bool IsNull( void ) const { return !m_pPtrInfo || m_pPtrInfo->IsNull(); } T* GetPtr( void ) const { return m_pPtrInfo ? m_pPtrInfo->GetPtr<T>() : nullptr; } T& operator*() const { assert( !IsNull() ); return *GetPtr(); } T* operator->() const { assert( !IsNull() ); return GetPtr(); } operator T*() const { return GetPtr(); } int GetRefCount( void ) const { return m_pPtrInfo ? m_pPtrInfo->GetRefCount() : 0; } }; template<class T> class WeakPtrController { WeakPtr<T> m_WeakPtr; public: nonvirtual ~WeakPtrController() { if ( m_WeakPtr.m_pPtrInfo ) { m_WeakPtr.m_pPtrInfo->m_Ptr = nullptr; } } explicit WeakPtrController( T* ptr ) : m_WeakPtr( new PtrInfo( ptr ) ){} WeakPtrController() = delete; WeakPtrController( const WeakPtrController& other ) = default; WeakPtrController& operator=( const WeakPtrController& other ) = default; template<class U> WeakPtr<U> GetDowncasted_unsafe( U* p_this ) { (void)p_this; static_assert( std::is_base_of<T,U>::value, "" ); return WeakPtr<U>( m_WeakPtr.m_pPtrInfo ); } WeakPtr<T> GetWeakPtr( void ){ return m_WeakPtr; } }; #define DEF_WEAK_CONTROLLER(type_) \ public: WeakPtr<type_> GetWeakPtr( void ){ return m_WeakPtrController.GetWeakPtr(); } \ protected: WeakPtrController<type_> m_WeakPtrController{this} #define DEF_WEAK_GET(type_) public: WeakPtr<type_> GetWeakPtr( void ){ return m_WeakPtrController.GetDowncasted_unsafe( this ); }class{}
特徴
- クラス定義時に仕込みを行う必要あり
- インスタンスの作成時に特別なことをする必要なし
- 自動変数でも利用可能
- アップキャストやダウンキャストにも対応
- shared_ptrやunique_ptrと組み合わせて使うことも可能
使い方
#include "WeakPtr.h" class A { DEF_WEAK_CONTROLLER( A ); // WeakPtrを作れるようにするための仕込み public: virtual ~A(){} void Print( const char* p_mes ) const { printf( "%s\n", p_mes ); } }; class B : public A { /* 基底クラスAですでに仕込みが行われているので不要 DEF_WEAK_CONTROLLER( B ); */ DEF_WEAK_GET( B ); // 必須ではないが、これを仕込んでおくと GetWeakPtr() が派生クラス型になる public: }; class X {}; int main() { // 基本的な使い方 { WeakPtr<A> weakA; { A a; weakA = a.GetWeakPtr(); if ( A* pA = weakA.GetPtr() ) // このようにif文でポインタを受け取るとオブジェクトの生存チェックが同時にできて楽 { pA->Print( "[1]" ); } if ( A* pA = weakA ) // T*への暗黙の型変換あり { pA->Print( "[2]" ); } } if ( A* pA = weakA.GetPtr() ) // aは破棄されたのでNULLポインタが返る { pA->Print( "[2] Not print" ); } } // アップキャスト { B* pB = new B; WeakPtr<B> weakB = pB->GetWeakPtr(); /* 暗黙の変換によるアップキャスト WeakPtr<A> weakA = pB->GetWeakPtr(); // 変換演算子を用意していないのでコンパイルエラー */ // 専用関数(GetUpcasted)でのアップキャスト WeakPtr<A> weakA = pB->GetWeakPtr().GetUpcasted<A>(); // 基底クラスの方のGetWeakPtr()を使うことでアップキャストせずに基底クラスのWeakPtrを取得することも可能 WeakPtr<A> weakA2 = pB->A::GetWeakPtr(); /* 無関係なクラスへのアップキャストはコンパイル時にエラー WeakPtr<X> weakX = pB->GetWeakPtr().GetUpcasted<X>(); */ } // ダウンキャスト { B* pB = new B; WeakPtr<A> weakA = pB->GetWeakPtr().GetUpcasted<A>(); /* 暗黙の変換によるダウンキャスト WeakPtr<B> weakB2 = weakA; // 変換演算子を用意していないのでコンパイルエラー */ // 専用関数(GetDowncasted)でのダウンキャスト WeakPtr<B> weakB2 = weakA.GetDowncasted<B>(); // 実行時にダウンキャストが行われる。weakAの参照先がBのインスタンスでない場合はダウンキャストに失敗しweakB2はNULLになる。 assert( weakB2.GetPtr() != nullptr ); // 今回はweakAの参照先がBのインスタンスなので、NULLにはならない。 // ダウンキャスト失敗例 { A* pA = new A; WeakPtr<A> weakA2 = pA->GetWeakPtr(); WeakPtr<B> weakB3 = weakA2.GetDowncasted<B>(); // BはAの派生クラスなのでコンパイルは通るが、実行時にダウンキャストに失敗し、weakB3はNULLになる。 assert( weakB3.GetPtr() == nullptr ); } /* 無関係なクラスへのダウンキャストはコンパイル時にエラー WeakPtr<X> weakX = weakA.GetDowncasted<X>(); */ } // shared_ptrやunique_ptrと組み合わせて使うことも可能 { std::shared_ptr<B> sharedB = std::make_shared<B>(); WeakPtr<B> weakB = sharedB->GetWeakPtr(); assert( weakB.GetPtr() != nullptr ); sharedB.reset(); assert( weakB.GetPtr() == nullptr ); { std::unique_ptr<B> uniqueB(new B); weakB = uniqueB->GetWeakPtr(); assert( weakB.GetPtr() != nullptr ); } assert( weakB.GetPtr() == nullptr ); } return 0; }
基本的な使い方は以下のとおりです。
- 弱参照を得られるようにしたいクラス定義に
DEF_WEAK_CONTROLLER
を仕込む - そのクラスのインスタンスからGetWeakPtr()で弱参照(WeakPtr<T>)を取得
- WeakPtr<T>::GetPtr()でポインタを取得( 参照先のオブジェクトが削除されていたらNULLが返る )
その他細かいところ
DEF_WEAK_CONTROLLER
を仕込んだクラスの派生クラスにはDEF_WEAK_CONTROLLER
は不要DEF_WEAK_CONTROLLER
を仕込んだクラスの派生クラスにDEF_WEAK_GET
を仕込むことで、GetWeakPtr()
が派生クラス型になるGetUpcasted<T>
を使うことでT型へアップキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる)GetDowncasted<T>
を使うことでT型へダウンキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる。型が相応しくても実行時の型が相応しくない場合はNULLになる)
解説
クラス一覧
- WeakPtr : 弱参照クラスです
- WeakPtrController : 弱参照を生成するためのクラスです。弱参照を扱えるようにしたいクラスにメンバ変数として持たせます
- PtrInfo : ポインタ情報です。こWeakPtrControllerとWeakPtr間で共有されます
弱参照が自動で無効化される仕組み
各弱参照は参照先オブジェクトへのポインタを直接持つのではなく「参照先オブジェクトへのポインタを持つクラス」へのポインタを持つようにします。実際のクラス名で説明すると、PtrInfo
が実際の参照先オブジェクトへのポインタを持ち、WeakPtr
はそのPtrInfo
へのポインタを持つという形になります。このPtrInfo
は同じ参照先を表すWeakPtr
間で共有されるので、PtrInfo
が持つ参照先オブジェクトへのポインタがNULLになると、そのPtrInfo
を共有しているすべてのWeakPtr
でオブジェクトへの参照がNULLになるという仕組みです。
このPtrInfo
に参照先オブジェクトのポインタをセットしたり、NULLにしたりするのがWeakPtrController
の仕事です。また、WeakPtr
を生成するのもWeakPtrController
の仕事です。
WeakPtrController
は初期化時にコンストラクタで引数として受け取ったポインタを「PtrInfo
が持つ参照先オブジェクトへのポインタ」にセットします。そしてデストラクタでNULLをセットします。なので、WeakPtrController
をメンバ変数として持たせた上で初期化時にthisポインタを渡すことで、「WeakPtrController
をメンバ変数として持ったクラス」が破棄されるときに自動で「PtrInfo
が持つ参照先オブジェクトへのポインタ」にNULLがセットされます。これで、オブジェクトが死ぬとそのオブジェクトを参照していた弱参照が無効化されるという仕組みの完成です。
PtrInfo
、WeakPtr
、WeakPtrController
の関係を図にするとこんな感じです。
PtrInfo
を共有する仕組み
先ほど、WeakPtr
がPtrInfo
を共有すると説明しました。さて、あるオブジェクトを共有するには「オブジェクトの所有権」、つまり、オブジェクトの生存期間の管理ができる必要があります。例えば、オブジェクトを自動変数として生成してしまうとオブジェクトの生存期間をそのスコープに限れられてしまうので、オブジェクトの所有権を持っているとは言えなくなります。
オブジェクトの所有権を持つには、基本的にはnewでオブジェクトを生成する必要があります。WeakPtr
で共有するPtrInfo
も、所有権を管理するためにnewで生成されます。
問題はnewしたPtrInfo
をいつdeleteするかです。PtrInfo
は共有されるものなので、最後の所有者がPtrInfo
を放棄するときにdeleteすべきです。これはまさにstd::shared_ptr
のような挙動です。そしてPtrInfo
は、実際にstd::shared_ptr
のように参照カウンタで自動でdeleteされるようになっています。
WeakPtr
とWeakPtrController
は生成/破棄のたびに、共有するPtrInfo
の参照カウントを加算/減算します。そして減算時に参照カウントが0になったらPtrInfo
をdeleteします。
つまり、弱参照の内部の実装に強参照が使われているというわけです。ややこしいですけど。
使い勝手を良くする工夫
上記、「弱参照が自動で無効化される仕組み」と「PtrInfo
を共有する仕組み」で弱参照クラスとして最低限の機能が実現できますが、使い勝手を良くするために以下のようなものも用意しました。
- WeakPtrControllerの用意を楽にするDEF_WEAK_CONTROLLERマクロ
- 派生クラスでは派生型の
WeakPtr
を取得できるようにするためのDEF_WEAK_GETマクロ - WeakPtrをポインタのように扱えるようにするための演算子オーバーロード(->,*,変換演算子)
- WeakPtrをアップキャスト/ダウンキャストするための関数
アップキャスト/ダウンキャストでは、static_assert
とstd::is_base_of
を使うことで、そもそも継承関係にない型への変換はコンパイル時にエラーになるようにしました。C++11様様です。
改善案
今回作った弱参照クラス、いくつか改善できる箇所があります。
- PtrInfoをプールで使い回すようにする
- ムーブセマンティクス対応
- マルチスレッド対応
現在の実装ではPtrInfoをnewでヒープメモリから確保するようにしています。これは、PtrInfoを共有できるようにヒープなどの独立したメモリ上に配置する必要があるためですが、PtrInfoのサイズは固定なのでPtrInfoのプールを用意することで高速化とメモリの断片化の回避になります。
プールを使用するとPtrInfoの個数に上限ができてしまいますが、PtrInfoは64bit環境でも1つ16バイトなのでプールサイズをかなり大きめにしても問題ないでしょう。
WeakPtr
のコピーは、PtrInfoへのポインタのコピーと参照カウントの増減くらいなので、たいした処理コストではないと思いますが、それでもムーブに対応すればPtrInfoへのポインタのコピーだけで済むようになるので結構おいしいかもしれません。
今回の実装ではマルチスレッドが全く考慮されていません。同じ参照を指す複数のWeakPtrが別々スレッドに存在する場合まともに動かないはずです。マルチスレッド対応するのか、マルチスレッド対応バージョンのWeakPtrを別に用意するのか、それともマルチスレッド非対応を明確にし別スレッドを検知したらabortするようにするのか…、何かしら対策は必要かもしれません。