2006年02月13日

コンストラクション(Cクラス)

次に、Cクラスのオブジェクトのコンストラクションに話を移しましょう。オブジェクトのコンストラクションに絡めて、クリーンアップスタックについて見ていきます。

 前回の記事でも多少触れさせていただきましたが、クラスのインスタンスを作成する時は通常、「2フェーズコンストラクション」を行います。2フェーズコンストラクションは、具体的には

* new(ELeave)演算子を用いてクラスのインスタンスが必要とするメモリを確保する。この新たにオーバーロードされている演算子はリーブを発生させる可能性がある。

* 作成したインスタンスが他のオブジェクトを所有する場合(つまりhas-aポインタを持つ場合)は、概ねConstructLという名前の関数でポインタの参照先インスタンスの生成とポインタの値の設定を行う。

 という流れになります。若干厄介に見えますが、これはメモリリーク対策になります。またまた繰り返しになりますが「メモリの確保が最もリソース不足によるエラーを起こしやすい」に注意してください。

 それでは、次の図を見てください。


 携帯環境ですから、非常にリソースが限られています。ですから、小さなオブジェクトの確保でも失敗をする可能性があるわけです。もし、上記のようなコードで、ptr2の初期化に失敗する可能性を考えるのであれば(これは携帯環境では十分に考えられる)、各々のポインタに対して、nullチェックを行う必要があります。これでは、プログラムを書くのが手間になってしまいます。また、リーブの機構があるのですからこれを使わない手はありません。

 そこで、例外処理をすっきりさせるため、先ほど出てきたリーブの仕組みを使うことを考えます。つまり、new(ELeave)を用いてメモリの確保を行います。このオペレータは先述の通り、メモリ確保に失敗するとリーブを発生させます。

 この場合、例外処理はすっきりしますが、新たな問題を引き起こします。それでは、次の図を見てください。


 つまり、リーブが発生した時点でTRAPマクロやTRAPDマクロで処理される箇所までスタックを遡るわけです。2回目のnew(ELeave)でリーブが発生した場合、ptr1が一時的な作業用のauto変数(コンストラクタの中であったとしても十分にあり得る)であれば、そのポインタが永遠に失われ、結果としてメモリリークの原因になります。

 そこで、次のような対策を講じます。

1. CBaseクラスを継承したクラス(ClassA)の使用するメモリのみを確保する(コンストラクタでは何も行わない)。メモリの確保に失敗すれば、リーブを発生させる。

2. ClassAによって所有される他のクラス(has-aポインタでの参照先)は、他のコンストラクション用の関数(一般にConstructL)で初期化する。ここでもリーブが発生する可能性がある。

3. 1または2でリーブが発生した場合、構築途中のオブジェクトへのポインタが失われないようにグローバルな領域(スタック)にポインタの値をコピーしておく(このスタックをクリーンアップスタックと呼ぶ)。

 それでは、2フェーズコンストラクションのサンプルを見てみましょう(Construction.zip)。このサンプルでは、自前でCBaseクラスから派生をしたCMyObjクラスを作成し、このクラスが2フェーズコンストラクションを行います。ただし、意図的にConstructLの関数内でリーブを発生させてる点に注意してください。

 サンプルをビルドしたのち、下記のアイコンのアプリケーションを実行します。


 すると、次のような画面が現れます。


 その後、メニューから「テスト」を選択します。ここでは意図的にリーブを発生させていますので、次のような画面が現れます。


 最後に次のような画面になります。


 ここで、今回のサンプルコードの重要な部分を確認しておきましょう。

CMyObj* CMyObj::NewL(CConstructionContainer* aContainer )
{
CMyObj* ptr = new(ELeave)CMyObj; // インスタンスが使用するメモリの確保
CleanupStack::PushL( ptr );
// (1)以下の処理でリーブが発生したときに備えクリーンアップスタックにpush
ptr->ConstructL(aContainer); // インスタンスのメンバの初期化
CleanupStack::Pop();
// リーブの可能性がなくなったので、クリーンアップスタックからpop
return ptr ;
}

void CMyObj::ConstructL(CConstructionContainer* aContainer)
{
this->iContainer = aContainer ;
TInt reason = KErrNoMemory; // 勝手にメモリがないことにする
User::Leave(reason); // リーブを発生(2)
aContainer->UpdateMessage(_L("初期化完了") ); // この行は実行されない
}

CMyObj::~CMyObj()
{
this->iContainer->UpdateMessage(_L("CMyObj 削除完了"));
}

 ここでは(1)にて構築途中のオブジェクトをクリーンアップスタックにプッシュし、第二フェーズのコンストラクションの(2)において意図的にリーブを発生させています。ここでクリーンアップスタックの重要な動作として、

リーブが発生したとき、クリーンアップスタックに詰まれた
全てのオブジェクトのデストラクタが呼び出される

という点に注意してください。つまり、2フェーズ目で構築に失敗し、リーブが発生しても、クリーンアップスタックにメモリ確保時に取得したポインタを積んでおけば、自動的にデストラクタが呼び出されるということです。そして、リーブ時にデストラクタを呼び出すためにCBaseクラスに仮想デストラクタが定義されているわけです。今回のサンプルコードの画面遷移でも、デストラクタが呼び出されていることが確認できると思います。

 ただし注意点として、リーブ発生時にはクリーンアップスタックに詰まれた全てのオブジェクトがデストラクトされます。つまり、

リーブが発生する可能性が無くなった時点でクリーンアップスタックから取り除く

必要があります。将来、全く別の理由でリーブが発生した場合、問題のないオブジェクトまでデストラクションする可能性があるためです。

次回の予定

 次回は、非同期の処理を見て生きたいと思います。Series60でもスレッドの利用は可能ですが、リソースの消費用の大きさから異なるフレームワークを使用します。この点について確認していく予定です。
posted by シンビアン at 20:58| Comment(0) | TrackBack(0) | Series60プログラミングテクニック | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/13243175

この記事へのトラックバック
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。