2007 年 9 月 7 日 23 時 55 分

C++/CLI #7: IDisposable と delete


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


突如現れた delete 構文。
この delete の意味するのは何なんだろうか。

まず、昨日のコードから、大事な部分を引用していこう。

    Graphics ^g = Graphics::FromHdc(...);
    try {
        ...
    } finally {
        delete g;
    }

結論から言うと、この delete は、
Dispose() の呼び出しと同じ役割を持つ。
より正確には、delete するハンドルが指すオブジェクトが、
IDisposable を実装している場合、
IDisposable の Dispose() を確実に呼び出す。

C# で書くと、以下のような処理に近いか。

        Graphics g = Graphics.FromHdc(...);
        try {
            ...
        } finally {
            IDisposable d = g as IDisposable;
            if (d != null) d.Dispose();
        }

C++/CLI では、IDisposable を特別なものとして扱い、
言語構文の中に組み込むことを選択したのだ。

そもそも IDisposable とは何か。
そして、なぜ特別扱いしているのか。

まず、C++ のオブジェクトの寿命は、
コードで示す有効期間(スコープ等)と常に等しい。
範囲外になると直ちにデストラクタが呼び出されるため、
破棄のタイミングはプログラマが制御できる。

それに対し、CLI のオブジェクトは
CLR のメモリ管理機構の監視下にあるため、
範囲外になってもすぐにオブジェクトは破棄されない。

CLI オブジェクトの後始末は、ファイナライザで行われるが、
このファイナライザが呼び出されるのは、
CLR のガベッジコレクションが発生した時であり、
どのタイミングで呼び出されるのかは予測できず、
オブジェクトの寿命が、コードで示す期間に一致しない。

CLR の挙動は、ファイナライザの呼び出しをまとめることで、
メモリと割り当てとリサイクル効率を最適化し、
さらにオブジェクトの後始末によって発生する、
オーバーヘッドを最小化できるというメリットはある。

ただし、プログラマが自身で定めたタイミングで、
確実にオブジェクトを破棄したい際には困ったことになる。

例えば、ファイルやソケットなど、
OS のハンドルやリソースを扱うクラスの場合、
オブジェクトが不要になった瞬間にそれを破棄し、
ハンドルやリソースへの参照を解放したい場合がある。

この場合、ファイナライザを使うのは妥当ではないため、
それらクラスには、リソースの解放を行うメソッドを用意し、
プログラマがそれを呼び出すことで、解放を行うしかない。

CLI の IDisposable インタフェースは、
上記の問題に対するデザインパターンとなっている。
IDisposable には Dispose というメソッドがあり、
これを呼び出すことで、オブジェクトの扱っている、
低水準のハンドルやリソースをその場で解放させるのだ。

IDisposable の Dispose を呼びだすことで、
CLI のオブジェクトの寿命を C++ と同じように、
コードで示す有効期間に等しく保てるのである。

IDisposable の構文に delete を使うことにしたのは、
C++ ネイティブの構文に似ているからであり、
C++ プログラマにも馴染みのある構文だからなのだ。

C++ では、new で作成したオブジェクトは、
delete で削除することで、デストラクタが呼び出され、
その場でオブジェクトの後始末ができる。

delete はオブジェクトの占めるメモリ領域も解放するので、
この場合は必ず delete で削除しなければならない。

    NativeClass *object = new NativeClass(123);
    object->Xxxx();
    delete object;

C++/CLI でも、gcnew で作成したオブジェクトは、
delete で削除することで、Dispose() が呼び出され、
その場でオブジェクトの後始末ができる。

オブジェクトの占めるメモリ領域は自動的に管理されるので、
マネージオブジェクトの場合は、delete は必須ではないが、
オブジェクトの寿命に関する考え方を統一化できる。

    ManagedClass ^object = gcnew ManagedClass(123);
    object->Xxxx();
    delete object;

なお、C++ では、スタック上に作成したオブジェクトは、
そのスコープを外れるタイミングで、
デストラクタが呼び出される。

    {
        NativeClass object(123);
        object.Xxxx();
    } // object のデストラクタが呼びだされる

C++/CLI でも、同じ構文でスタック上に作成できるが、
マネージオブジェクトの場合、
実際にはスタック上のメモリには確保されず、
内部で gcnew されてマネージヒープ上に確保される。

そして、構文上のスコープを外れるタイミングで、
Dispose() が呼び出される。

    {
        ManagedClass object(123); // 内部で gcnew され
        object.Xxxx();
    } // object の Dispose() が呼びだされる

これは、C# の using 構文に近いが、
C# の using 構文では、その対象となる型は、
IDisposable を実装してなければならない。

それに対し、C++/CLI のスタック構文や delete では、
どのようなオブジェクトに対しても使うことができ、
IDisposable でないオブジェクトでは単に無視される。

C++/CLI は、IDisposable の構文をうまく隠すことで、
C++ との構文上・動作上の互換性を保つことに成功している。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.