値型と違い、参照型のインスタンスは全てCLIの管理するマネージヒープ上にしか存在できません。そしてC++/CLIでは、通常それをハンドルという形で取り扱うわけです。
using System::Object; Object^ o = gcnew Object; |
しかし、これ以外の手法も用意されました。その最たるものが自動変数の構文です。
using System::Collections::ArrayList; ArrayList arr; |
このように普通のC++の自動変数のように宣言することができます。ただし内部ではgcnewされるコードが生成されます。もちろんメンバの参照にはピリオドを使います。
arr.Add(1); arr.Add(2); arr.Add(3); |
この自動変数構文には、いちいちgcnewしなくてよいということのほかに、もう1つ重要な特長があります。それは、オブジェクトがスコープを抜けるときにデストラクタが呼ばれるということです。
mallocとfree、GetDCとReleaseDCのように獲得と解放処理が対になって用意されている「資源」(リソース)を扱う際には、何より解放処理を行うことが重要です。C++では、解放処理をデストラクタに書くことでそれに対処してきました。変数がスコープを抜けるときには必ずデストラクタが呼ばれるため、確実に解放処理が行われます。この技法は、広義にRAIIと呼ばれます。そして、スコープを抜けるときにデストラクタが呼ばれるということは、RAIIがC++/CLIでも引き続き行えるということを意味します。
C++/CLIの参照クラスでは、デストラクタとは別にファイナライザを定義できます。
ref class Foo //参照クラス { public: ~Foo(); //デストラクタ !Foo(); //ファイナライザ }; |
ファイナライザは、ガベージコレクションでオブジェクトが収集される直前に呼ばれます。実はこれが、Object.Finalizeメソッドのオーバーライドであり、C#のデストラクタに相当するものです。
実のところC++/CLIのデストラクタは、IDispose.Disposeメソッドのオーバーライドに相当します。
次のC#のコードが、
using System.IO; void f() { using (FileStream fs = new FileStream("Foo", FileMode.Read)) { // ... } } |
C++/CLIでは次のようになります。
using namespace System::IO; void f() { FileStream fs("Foo", FileMode::Read); // ... } |
usingをいちいち書かなければならないC#のコードは冗長に見えますが、(C++/CLIではない素の)C++と違って基本的にメモリを管理する必要がないので、usingを使用する箇所は限られます。逆にC++ではメモリもデストラクタで解放するスタイルが薦められており、スコープと結び付けられたデストラクタを持ったクラスはそこら中に溢れています。ただし、参照カウント式にすると循環参照を気を付けなければならないのが玉に瑕ですが。
ところで、デストラクタが呼ばれると、ファイナライザは呼ばれないことに注意が必要です。
しかし、自動変数として宣言できない場合はどうなるでしょう?その答えとしてVisual C++ 2005にはauto_handleというクラスが存在します。これは、ハンドルを保持して、自身のデストラクタでそれをdeleteするクラスです。
System::IO::FileStream^ func() { return gcnew System::IO::FileStream("Foo", FileMode.Read); } int main() { msclr::auto_handle<System::IO::FileStream> fs = func(); } |
実はマネージ型ハンドルにdelete演算子を使用すると、(あれば)デストラクタが呼ばれます。無論、そのままではRAIIの恩恵はありません。しかし、deleteを更にラップしてしまえば良いわけで、それがauto_handleです。deleteをラップしたというのが、なんだか素のC++のメモリ管理と似たような感じがします。
今回リソース管理の機能を特に取り上げる結果になりました。高水準な機能の揃った環境では、リソース管理が命です。借りたものはきちんと返していないと、思わぬところで足をすくわれることがあるかもしれません。
スポンサード リンク |
この記事のカテゴリ
- C++/CLI ⇒ C++/CLI (4) 参照クラスとリソース管理