僕の最近のプログラミングでは、どうも pimpl イディオムを使う事が多いです。
最近のプログラミング、って、まぁゲーム作ってるんですけど、やっぱりゲームみたいな大きなプロジェクトだと分割コンパイルは必須なんですよね。
そうなると、どうしても pimpl イディオムを使って各クラスの実装をソースファイルに隠蔽したくなってくるわけです。
クラスのデータメンバがちょっと変わったくらいで全部コンパイルしなおすのって、正直苦痛以外の何者でもないわけですし、かといって virtual を使うのはポインタの扱いが面倒なので、まぁ当然といや当然。
しかし、このpimplイディオムって、地味に面倒なんです。何が面倒って、普通ならコンパイラが自動定義してくれるコピーコンストラクタ、代入演算子、デストラクタを、いちいち自分の手で書いていかなきゃならない点。
正直、そんな処理なんて定形文なんだから、全部自動でやってくれる方が良いに決まってる。
ってなわけで、そんな感じの処理を自動化してくれるポインタを作ってみたので紹介。
では早速、ソースコードを見てもらいましょう:
#include <algorithm> #include <cassert> #include <boost/intrusive_ptr.hpp> #include <boost/noncopyable.hpp> namespace gintenlib { // boost::intrusive_ptrを使うためのオブジェクトを製作するテンプレート // shared_ptr だとオーバースペックに感じたので、今回は使いません template<typename Derived> class reference_counter : boost::noncopyable { typedef reference_counter base_; public: void AddRef() const { ++count; } void Release() const { if( --count <= 0 ) { delete static_cast<const Derived*>(this); } } protected: reference_counter( int initial_count = 0 ) : count(initial_count) {} ~reference_counter(){} private: mutable int count; }; template< typename T > inline void intrusive_ptr_add_ref( const reference_counter<T>* ptr ) { ptr->AddRef(); } template< typename T > inline void intrusive_ptr_release( const reference_counter<T>* ptr ) { ptr->Release(); } // クローン製造機 // operator()で渡されたポインタの正確な複製を作成します // また、任意のポインタを、newで生成された時の型に戻した上でdeleteしてくれます // ただし、予めコンストラクタで型情報を教えておく必要あり class cloner { public: cloner(){} template<typename T> cloner( T* ptr ) : p( ptr ? new impl<T>() : 0 ) {} template<typename T> T* operator()( T* ptr ) const { return p ? static_cast<T*>( p->clone(ptr) ) : 0; } template<typename T> void destruct( T* ptr ) const { if(p) { p->destruct(ptr); } } // 後で必要 void swap( cloner& other ) // never throws { p.swap( other.p ); } private: // まー、要するにboost::function的な処理です struct impl_base : reference_counter<impl_base> { impl_base(){} virtual ~impl_base(){} virtual void* clone( const void* ) const = 0; virtual void destruct( const void* ) const = 0; }; template< typename T > struct impl : impl_base { typedef const T* pointer; static pointer cast( const void* ptr ) { return static_cast<pointer>(ptr); } void* clone( const void* ptr ) const { using namespace std; assert(ptr); return new T( *cast(ptr) ); } void destruct( const void* ptr ) const { delete cast(ptr); } }; // コピーが楽なように intrusive_ptr を使う boost::intrusive_ptr<const impl_base> p; }; // class cloner // 本体 // 「深いコピー」を実現するポインタ template<typename T> class cloning_ptr { // 割と重要な friend 宣言 template<typename U> friend class cloning_ptr; public: // type definitions typedef T element_type; typedef T value_type; typedef T* pointer; typedef T& reference; // デフォルトでは NULL セット cloning_ptr() : p(0), c() {} // 最重要部分。テンプレートにすることで ptr の型情報を受け取れる template<typename U> explicit cloning_ptr( U* ptr ) : p( ptr ), c( ptr ){} // 他の型でも暗黙変換さえ通ればOK // 地味ですが cloner によってコピーしています // cloner 内部では、new で渡された型へと static_cast してから // コピーコンストラクタを呼び出しているので、確実に同じものが生成できます template<typename U> cloning_ptr( const cloning_ptr<U>& src ) : p( src.c(src.p) ), c( src.c ) {} // 上のテンプレートとは別にコピーコンストラクタも用意する必要あり cloning_ptr( const cloning_ptr& src ) : p( src.c(src.p) ), c( src.c ) {} // cloner に頼んで p を削除してもらいます // やはりこれも、newで渡された方へとキャストしてから削除してるので、安全に破棄出来ます ~cloning_ptr() { c.destruct(p); } // コピーして swap イディオムのために必要 void swap( cloning_ptr& other ) // never throws { std::swap( p, other.p ); c.swap( other.c ); } // 定型通りのoperator= cloning_ptr& operator=( const cloning_ptr& src ) { cloning_ptr temp(src); this->swap(temp); return *this; } template<typename U> cloning_ptr& operator=( const cloning_ptr<U>& src ) { cloning_ptr temp(src); this->swap(temp); return *this; } // NULLにする void reset() { cloning_ptr temp; this->swap(temp); } // 新しくポインタを受け取る。昔のオブジェクトは破棄 template<typename U> void reset( U* ptr ) { cloning_ptr temp(ptr); this->swap(temp); } // dereference pointer operator->() const { // assertが関数でもマクロでも良いように using しておく using namespace std; assert( p ); return p; } reference operator*() const { using namespace std; assert( p ); return *p; } pointer get() const // never throws { return p; } // その他、get()とか ***_pointer_cast() とか operator safe_bool() とか必要なものを private: pointer p; cloner c; }; // cloning_ptr<T> } // namespace gintenlib
はい、長かったですが以上です。
割と手の込んだことをやってますが、これは確実にコピーを行うため。
肝は cloner です。こいつがポインタを本来の型にキャストしてくれるお陰で、どのような状況になっても同じものを正確に複製したり、確実に本来のデストラクタを呼び出したりできるのです。
その詳しい仕組みはソースコードを眺めてもらうとして、pimplイディオムに使うには、もう少し改善すべき点があるので改善しておきます。
namespace gintenlib { // pimplイディオム用ポインタ template<typename T> class pimpl_ptr : public cloning_ptr<T> { typedef cloning_ptr<T> base; public: // type definitions typedef T element_type; typedef T value_type; typedef T* pointer; typedef const T* const_pointer; typedef T& reference; typedef const T& const_reference; // pimpl の場合、デフォルト初期化は禁止した方がバグが出にくいと考える // pimpl_ptr() {} // コンストラクタは同じ template<typename U> explicit pimpl_ptr( U* ptr ) : base(ptr) {} // コピーコンストラクタ、デストラクタはベースクラスのをそのまま使う // 代入に関しては、テンプレート版は不要 void swap( pimpl_ptr& other ) // never throws { base::swap( other ); } pimpl_ptr& operator=( const pimpl_ptr& src ) { pimpl_ptr temp(src); this->swap(temp); return *this; } // こいつも不要かもしれないが、一応定義しておく template<typename U> void reset( U* ptr ) { pimpl_ptr temp(ptr); this->swap(temp); } // 参照外しを、トリッキーに // こうすることで、const 関連のバグを減らせる const_pointer operator->() const { using namespace std; assert( base::get() ); return base::get(); } pointer operator->() { using namespace std; assert( base::get() ); return base::get(); } const_reference operator*() const { using namespace std; assert( base::get() ); return *base::get(); } reference operator*() { using namespace std; assert( base::get() ); return *base::get(); } const_pointer get() const // never throws { return base::get(); } pointer get() // never throws { return base::get(); } }; // pimpl_ptr<T> } // namespace gintenlib
やってることは、pimplとして使う分には起こりえない変換を無くし、かつ参照はずし関連の動作をpimpl用に最適化した感じです。使い方は:
class hoge { public: hoge(); hoge( int i ); // コピーコンストラクタ、デストラクタ、代入動作は不要 // const と non_const あたりの動作を確かめたい void reset( int i ); void foo() const; private: class impl; gintenlib::pimpl_ptr<impl> p; }; #include <iostream> using namespace std; int main() { hoge h1, h2(2); const hoge h3(3); h1.foo(); h2.foo(); h3.foo(); // いろいろ変えてみる h1 = h2; h2.reset(0); // これは当然ダメ // h3 = h2; /*とか*/ h3.reset(9); // とか h1.foo(); h2.foo(); } struct hoge::impl { impl( int i_ ) : i(i_) {} // コピーコンストラクタ、デストラクタ、代入動作は不要 void reset( int i_ ) { i = i_; } void foo() const { cout << "hoge(" << i << ")::foo has called.\n"; } private: int i; }; hoge::hoge() : p( new impl(0) ) {} hoge::hoge( int i ) : p( new impl(i) ) {} void hoge::reset( int i ) { p->reset(i); } void hoge::foo() const { p->foo(); }
こんな感じ。pimpl_ptrの代わりにcloning_ptrを使った場合、hoge::reset(int)に const を付けてもコンパイル通ってしまいます。