スコープ管理機能付きのポインタクラス
C#には最初からスコープ管理機能付きのポインタがあるみたいですがC++にはありません。そこでC++用のそれを作りました。
実は今までC++をCの高機能版程度にしか使っていなかったのでC++の奥深さや面白さを知らなかったのですが、最近もっとC++らしいプログラムが組みたいと思うようになって10年以上前に買ったC++の参考書を改めて読んでみました。そしたらC++の言語仕様の奥深さと面白さにはまりました。
C++の正式な教科書である「プログラミング言語C++第3版」は分厚くて辞書的な内容なので分かりにくく言語仕様の面白さがよく理解できませんでしたが、参考書の方は読み物としても面白い内容になっているのでC++の奥深さや面白さが良く分かりました。読んだ本は「現実的なC++プログラミング」と「Efective C++ 改訂2版」という有名なC++の参考書です。
クラス名 template< class T > class TScopePtr< T >
これがスコープ管理機能付きポインタクラスの名前です。
このクラスを使用するときはコンストラクタで new や new[] で作成したオブジェクトのポインタを渡します。
このクラスオブジェクトは自動変数として宣言してください。そうすると自動変数はC++の言語仕様としてスコープから外れたときにデストラクタが実行される仕組みになっているので、そのときにコンストラクタで与えられたオブジェクトを削除するという単純な設計になっています。
オブジェクトの型は基本データ型や独自のクラスなど何でも良いのでテンプレートクラスにしてあります。
例を示します。
// ブロックの開始 { // TScopePtr コンストラクタに new 演算子で作成したオブジェクトポインタを渡します。 TScopePtr< int > p = new int; // TScopePtr 変数をオブジェクトポインタ型の変数として使用します。 * p = 12345; DebugPrint( _T( "* p = %d\n" ), * p ); } // ブロックの終了 // ここで TScopePtr 型変数 p が有効スコープから外れたので、 // 先ほど new 演算子で作成した int 型変数が自動的に削除されます。
解説
TScopePtr クラスは内部でオブジェクトポインタの参照回数を管理しています。
まずコンストラクタで参照回数が1に設定されます。
TScopePtr 変数自体は自動変数なのでスコープから外れると自動的にデストラクタが実行されます。デストラクタではオブジェクトポインタの参照回数をデクリメントします。そして参照回数がゼロになったときオブジェクトを削除します。
次に TScopePtr クラスのコピーについて説明します。
TScopePtr 変数を他の TScopePtr 変数にコピーした場合は、コピー先のオブジェクトポインタにコピー元のオブジェクトポインタをコピーしてその参照回数をインクリメントします。
そうするとコピー元の TScopePtr 変数がスコープから外れて消滅しても参照回数はゼロにならないためオブジェクトは削除されません。コピー先の TScopePtr 変数が消滅したときには参照回数がゼロになるのでオブジェクトが削除されます。
このようにして異なるブロック間でのスコープ管理機能付きオブジェクトポインタの共有が可能になります。
また TScopePtr 変数をなるべく素のオブジェクトポインタと同じように使用できるようにするためにいくつかの演算子オーバーロード関数を定義してあります。
T *(キャスト)演算子はオブジェクトポインタを返します。
*(アステリスク)演算子はオブジェクトの参照型を返します。
->(アロー)演算子はオブジェクトポインタを返すので、オブジェクトのメンバに直接アクセスできます。
[](配列添え字)演算子はオブジェクトの配列要素の参照型を返します。
ちなみに DebugPrint() は OutputDebugString() を書式付き文字列対応にしたデバッグコンソール出力関数です。
配列オブジェクトの場合
配列オブジェクトの場合はコンストラクタに TArrayPtr クラスオブジェクトを渡します。
TArrayPtr クラスは配列ポインタと配列要素数だけを持つ単純なクラスです。
当初は TScopePtr クラスに配列ポインタを直接渡す方向で設計していたのですが、結局こっちの方が実装がスマートになるのでこのようにしました。
例を示します。
const size_t uElements = 3; // 配列要素数 // ブロックの開始 { // TScopePtr コンストラクタに TArrayPtr クラスオブジェクトを渡します。 TScopePtr< int > p = TArrayPtr< int >( uElements ); // TScopePtr 変数をオブジェクトポインタ型の変数として使用します。 for ( size_t n = 0; n < uElements; n++ ) p[ n ] = n; for ( size_t n = 0; n < uElements; n++ ) DebugPrint( _T( "p[ %u ] = %d\n" ), n, p[ n ] ); } // ブロックの終了
次に多次元配列の場合の例を示します。
// 多次元配列型を定義します。 const size_t uElements1 = 3; const size_t uElements2 = 4; const size_t uElements3 = 5; typedef int ( TMyArray[ uElements1 ][ uElements2 ][ uElements3 ] ); // ブロックの開始 { // 多次元配列を単純配列として TScopePtr コンストラクタに TArrayPtr クラスオブジェクトを渡します。 TScopePtr< int > p = TArrayPtr< int >( uElements1 * uElements2 * uElements3 ); // TScopePtr 変数をオブジェクトポインタ型の変数として使用します。 for ( size_t n1 = 0; n1 < uElements1; n1++ ) { for ( size_t n2 = 0; n2 < uElements2; n2++ ) { for ( size_t n3 = 0; n3 < uElements3; n3++ ) { // 多次元配列型にキャストします。 TMyArray & data = reinterpret_cast< TMyArray & >( * static_cast< int * >( p ) ); data[ n1 ][ n2 ][ n3 ] = ( n1 * uElements2 + n2 ) * uElements3 + n3; } } } for ( size_t n1 = 0; n1 < uElements1; n1++ ) { for ( size_t n2 = 0; n2 < uElements2; n2++ ) { for ( size_t n3 = 0; n3 < uElements3; n3++ ) { TMyArray & data = reinterpret_cast< TMyArray & >( * static_cast< int * >( p ) ); DebugPrint( _T( "data[ %u ][ %u ][ %u ] = %d\n" ), n1, n2, n3, data[ n1 ][ n2 ][ n3 ] ); } } } } // ブロックの終了
生成消滅演算子管理クラスについて
TNewDelete クラスは生成消滅演算子管理クラスです。
グローバル new, delete, new[], delete[] 演算子をオーバーロードしてその記録を行っています。
TScopePtr クラスに渡されるオブジェクトポインタのエラーチェックのために実装していますが、もしエラーチェックをしないのであれば必要のないクラスです。
TScopePtr クラスに渡されたオブジェクトポインタが new または new[] 演算子で作成されたものかどうかをチェックしています。もしそうでなければ例外を発行します。
もしオリジナルクラスで生成消滅演算子をオーバーロードする場合は TNewDelete クラスに登録および削除を行ってください。
例を示します。
template< class T > class TMyClass { private : T m_objectA; ///< オブジェクトA T m_objectB; ///< オブジェクトB public : /// new 演算子オーバーロード関数 static void * operator new( size_t uSize ) { // メモリを確保します。 void * pBuffer = malloc( uSize ); // 生成消滅演算子管理クラスの単一オブジェクトリストに登録します。 TNewDelete::AppendSingle( pBuffer, uSize ); // メモリポインタを返します。 return pBuffer; } /// delete 演算子オーバーロード関数 static void operator delete( void * pBuffer ) { // 生成消滅演算子管理クラスの単一オブジェクトリストから削除します。 TNewDelete::DeleteSingle( pBuffer ); // メモリを解放します。 free( pBuffer ); } /// new[] 演算子オーバーロード関数 static void * operator new[]( size_t uSize ) { // メモリを確保します。 void * pBuffer = malloc( uSize ); // 生成消滅演算子管理クラスの配列オブジェクトリストに登録します。 TNewDelete::AppendArray( pBuffer, uSize ); // メモリポインタを返します。 return pBuffer; } /// delete[] 演算子オーバーロード関数 static void operator delete[]( void * pBuffer ) { // 生成消滅演算子管理クラスの配列オブジェクトリストから削除します。 TNewDelete::DeleteArray( pBuffer ); // メモリを解放します。 free( pBuffer ); } /// コンストラクタ TMyClass( const T & objectA = 0, const T & objectB = 0 ) : m_objectA( objectA ), m_objectB( objectB ) {} /// デストラクタ ~TMyClass() {} /// オブジェクトA参照アドレス取得関数 T & A() { return m_objectA; } /// オブジェクトB参照アドレス取得関数 T & B() { return m_objectB; } };
ソースファイル
ソースコードのコメントは Doxygen 仕様で書いてあるので Doxygen でドキュメントを自動生成できます。
コンパイラはソースコードの文法エラーを検出してくれますがコメントの不具合は検出しません。しかし Doxygen を使うと生成されたドキュメントを確認することでコメントの不具合が非常に分かりやすくなります。またコメントの不具合を修正していると、そこでソースコードのバグを発見することもあります。
あと参考書を読んで初めて知ったのですが、C++ではポインタに NULL を使っちゃだめらしいです、マナーとして。NULL は void * なので結局ほとんどのケースで型変換が必要になるから無意味だそうです。なので今回のソースコードでは NULL を使いたくなる部分をすべて 0 にしてあります。
以下に全ソースファイルを示します。