連載
» 2007年04月24日 00時00分 公開

Symbian OS開発の勘所(4):C++のメモリリークを防ぐフレームワーク (3/3)

[大久保 潤 管理工学研究所,@IT MONOist]
前のページへ 1|2|3       

メモリリークが起きるまで

 図4中の各関数の実行期間には、赤色で示された部分が存在します。この部分が例外発生時に処理をスキップされる所です。下記のようなサンプルコードだと5行目の「delete obj;」の行がスキップ領域に該当します(L付きルールによりFuncL()が例外を返す可能性があるため)。


void foo()
{
    CClass* obj = new CClass();
    FuncL( obj );
    delete obj;
}
オブジェクトがdeleteされない可能性

 「C++におけるメモリリークとは、newで確保されたオブジェクトが、何らかの理由によりdeleteされないことにより発生します」と第3回で説明しました。すると上記のコードでは、例外が発生したときに処理のスキップが行われ、結果としてメモリリークが発生することになります。これは大問題です。

避難所−CleanupStack

 例外が発生したとしてもヒープに確保されたインスタンスのポインタを忘れさえしなければ、いつか削除を行うことができる。これが例外処理の洪水を乗り切るためのアイデアの要諦です。そのためSymbian OSでは以下のような箱船を用意しました。

class CleanupStack
    {
    static void PushL( TAny* aPtr );
    static void PushL( CBase* aPtr );
    static void PushL( TCleanupItem anItem );
    static void Pop();
    static void Pop( TInt aCount );
    static void PopAndDestroy();
    static void PopAndDestroy( TInt aCount );
    ……
    } 

 CleanupStack::PushL()を経由してCleanupStackに積まれたインスタンスは、例外をキャッチするTRAPに到達したときに、必ず消されることが保証されます。これにより例外によるメモリリークを乗り切ることができます。

 またC++のポインタは参照カウンタ付きではないので、同一の領域に対する複数回のdeleteの発行は厳禁です。CleanupStackによる保護が不要になり次第、CleanupStack::Pop()を発行して対象インスタンスをCleanupStackの保護下から外すことが利用者に要求されます。

 意外に忘れられがちなことですが、インスタンスの正しい消し方は千差万別です。delete一発で足りるもの、Close()を明示的に発行しなければならないもの、単にメモリの解放で事足りるもの、それらの違いをCleanupStackはうまく吸収しています。インスタンスの型に応じてCleanupStack::PushL()が自動的に呼び分けられ(C++のオーバーロードによる)、適切に情報が積まれるので、CleanupStack経由で当該インスタンスを削除するときにはCleanupStack::PopAndDestroy()と呼び出すだけです。TRAPで完全にインスタンスの後始末ができるのもこの機能に依ります。

 CleanupStackの典型的な使い方は、例えば以下のようになります。

    CClass* o1 = new( ELeave ) CClass();  ……(A)
    CleanupStack::PushL( o1 );            ……(B)
    CClass* o2 = new( ELeave ) CClass();  ……(C)
    CleanupStack::Pop( o1 );              ……(D) 

 new(ELeave)は、インスタンス確保時にメモリ不足が発生すると例外を発生させる、多重定義(オーバーロード)タイプのアロケータです。そのため(C)の行ではメモリの状態に応じて例外が発生する可能性があります。

 しかしo2をnewする際に例外が発生したとしても、(B)のとおりCleanupStackにはすでにo1へのポインタが積まれています。そのため、どこかに必ず存在するTRAPに到達したとき、すでにCleanupStack::PushL()されているo1のインスタンスは消されることが保証されます。(D)の行に到達したときには例外が発生する可能性がなくなっています。そこでCleanupStack::Pop()を発行し、多重削除による障害を回避しています。

 以上が現代的なエラー処理機構である例外処理と、C++のメモリ管理のミスマッチに対してSymbian OSが提供しているフレームワークの概要です。

2フェイズコンストラクション

 クラスのインスタンスを確保するときには、2種類の例外が起き得ます。

  1. インスタンスの確保自体にかかる例外であるメモリ不足
  2. コンストラクタの中で発生し得る、インスタンスのセットアップにかかる例外

 これもメモリリークの原因となります。

    CClass* o1 = new( ELeave ) CClass(); 

 このようにインスタンスを生成する際、メモリ確保に成功した後にコンストラクタの中で例外が発生したとしたらどうなるでしょう。インスタンスは確保されているのにo1にはポインタが返ってこない。知らないポインタは消せない、ということでメモリリーク確定です。

 そこでSymbian OSでは以下のルールを設けて、インスタンス作成時のメモリリークを回避しています。

A.言語レベルの構築処理であるコンストラクタの中では例外が発生する処理を記述しない
→するとnew(ELeave)発行時の例外はメモリ不足に限定される

B.例外が発生する処理はConstructL()というユーザーレベルの構築処理に集約する

 さらにこのルールを徹底するために、

C.2つの構築処理を集約するNewL()およびNewLC()という名前のファクトリメソッドの提供を必須とし、インスタンスの作成に手順違いがないようにする

という第3のルールが存在します。これらを満たすコードの例を示します。

class   CClass : public CBase
    {
public:
    static  CClass* NewL(); // ファクトリメソッド。
    static  CClass* NewLC();// ファクトリメソッド。
    ~Class();               // 
private:
    CClass();               // リーブしない処理のみ。
    void    ConstructL();   // 第二フェーズの構築。
                            // リーブあり。
    ……
    }
static  CClass* CClass::NewLC()
    {
    CClass* self = new( ELeave ) CClass();
    CleanupStack::PushL( self );
    self->ConstructL();
    return self;
    }
static  CClass* CClass::NewL()
    {
    CClass* self = CClass::NewLC();
    CleanupStack::Pop( self );
    return self;
    } 
インスタンス作成時のルールを満たすコード例

 これが2フェイズコンストラクション(言語レベルとユーザーレベルの2段階の構築処理)のエッセンスのコードです。本質的にはNewL()だけでもよいのでしょうが、コンストラクションを実行した側でCleanupStack::PushL()を再度呼び出すコストが我慢ならなかったと見えて、CleanupStackにインスタンスを積みっぱなしで返すバージョンを用意することが推奨されています。それがNewLC()です。

 NewL()が用意されるのであれば言語のコンストラクタや、ConstructL()が勝手に呼び出せてしまうのは事故の原因にしかなりません。そこで上記コードのようにprivate修飾を行い、クラス外部に見えなくするのが一般的です。

 以上により、例外が発生したとしてもメモリリークを防ぐことが保証できます。しかも呼び出し側の「善意」を当てにする必要はありません。クラスの設計者が適切にメソッドを用意するだけで安全性が保たれるのです。

※コラム:メモリ管理のアイデア
C++が提供するメモリ管理とプログラムに要求されるロバストさのギャップを真剣に考えると、インスタンスの取得/解放とインスタンスをサービス可能にするためのタイミングを分離するというアイデアに到達するようです。
実は筆者が「桐」を開発しているときに同じ悩みを抱えて、最後にたどり着いたデザインもコンストラクタ・デストラクタとインスタンスの構築・破壊のタイミングを分離する(筆者の場合、Create()、Destroy()という名称にしました)、それの辻褄が合うようにクラスの継承関係や基本サービスをひっくり返すというものでした。
Symbian OSを最初に見たときには「ワタシはこれを知っている」という感想を強く持ったのを覚えています。


リソース管理とCleanupStack

 CleanupStackはPopAndDetroy()というAPIを持っており、これを呼び出すと適切にインスタンスの後始末を行う、ということを先に述べました。単にdeleteを行う、またメモリを解放するという場合はCleanupStack::PushL()でよいのですが、消す前にClose()を行わなければならない場合どうすればよいのでしょうか。

 そのケースでは以下のメソッドを経由してCleanupStackにインスタンスを積む必要があります。

void CleanupReleasePushL( T& aRef )
→PopAndDestroy()時にaRef.Release()の発行を保証する

void CleanupClosePushL( T& aRef )
→PopAndDestroy()時にaRef.Close()の発行を保証する

 これにより開いたファイルをクローズする、クライアントサーバの後始末をする、カーネルオブジェクトを解放するなど、必ず後始末が必要な処理を例外発生時にも漏れなく行うことができます。これがCleanupStackはリソース管理であるといわれるゆえんです。



 今回はSymbian OSにおけるリソース管理のフレームワーク、CleanupStackを紹介しました。CleanupStackは単にメモリの解放だけではなく、インスタンスが必要とする後始末を集約できます。このCleanupStackと2フェーズコンストラクション、それに前回説明したインスタンスの作成先をクラスの分類の主要な属性としてとらえるというアプローチ、これらを組み合わせることによりSymbian OSでは携帯電話やスマートフォンに必要な要求である

何があってもメモリリーク・リソースリークを起こさない


を実用的なコストの範囲で実現できるようになっています。

 次回は文字列や動的配列など、ユーザーサイドで可変長のオブジェクトを扱うための機構について解説を行いたいと思います。(次回に続く)


前のページへ 1|2|3       

Copyright © ITmedia, Inc. All Rights Reserved.