IDisposableとdelete

.NET Frameworkのマネージヒープ上に作成したインスタンスの開放は、ランタイムの仕事(ガーベッジコレクション=GC)になりました。GCは非同期で起動し、プログラマからはいつ動くか知ることはできません。そしてGCが動く際にそのクラスのファイナライザーが呼ばれます。

しかし、明示的に開放する方法も用意されています。明示的に開放できれば、ファイナライザーを呼ぶ必要がなくなります。ファイナライザーは別スレッドで動くため遅くなりますので、それを呼ばなければプログラムを高速化できます。

また、HWNDなどのスレッド所有のハンドルは他スレッドからDestroyWindowなどを呼んでも、ウインドウは破棄できないはずです。実際に新しく作成したフォームをのハンドルにnullptrを代入して、GC::Collect()を呼んでも、そのフォームは削除されません。

.NET Frameworkでは、IDisposableが用意されこれをを継承したクラスは、明示的に破棄できます。または破棄してほしいことを宣言します。IDisposableは以下のように宣言されています。

public interface IDisposable
{
   void Dispose(  );
}

C++/CLIでデストラクタを定義するとこのインターフェースのDisposeメソッドを実装したことになります。C++のデストラクタと違う点は、メモリの開放を行わないことです。デストラクタを定義していないクラスをdeleteした場合、コンパイラは何のコードも生成しません。

またDisposeメソッドを実装したからといって、それがコールされるかはわかりませんのでコールされた場合とされなかった場合とでファイナライザーの挙動も変えなければならないかもしれません。

ファイナライザーはObjectクラスで宣言されC++/CLIではクラスCのファイナライザーは!C()の構文で書きます。ファイナライザーが呼ばれているということはそのインスタンスはすでに誰からも参照されておらず、いわば死んだものです、さらにこのメソッドはスレッドで実行されているので、このメソッドでいろいろやってしまうとわけがわからなります。しかしそのインスタンスが確保したアンマネージリソースはもうここでしか解放することができないので、ここで開放することになります。

一般にはファイナライザーではマネージリソースに触るべきでないとされています。マネージリソースとはそもそも必要(参照)がなくなったら勝手に消えてくれるものなので、すでに死んでしまっている自分が触るのは適当でないのです。そして自分が死ぬタイミングは思ったよりも早く来ることもありえます。

ref class C {
    System::Collections::Generic::List<int>^ list;
public:
    C() {
        list = gcnew System::Collections::Generic::List<int>;
    }
    ~C() {}
protected:
    !C() {
        list->Clear();
    }
public:
    System::Collections::Generic::List<int>^ getList() {
        return list;
    }
};
int main()
{
    C^ c = gcnew C;
    System::Collections::Generic::List<int>^ list = c->getList();
    list->Add(1);
    list->Add(2);
    list->Add(3);
    System::Console::WriteLine(list->Count);
    return 0;
}

上記のコードでは、c→getList()が行われた後はcは参照されていません。よって、list→Add(1);が行われる前に!C()が動いてもいいわけです。この場合はファイナライザーでlistに触らなければいいわけですが、これがlistでなくアンマネージのポインタだと困ったことになります。ファイナライザーはそのポインタを開放しなければなりませんが、それをすると上のような問題があります。

よってアンマネージなメンバーを含むref classを作るときは設計の段階でいろいろ考えておかないと後々こまるかもしれません。ひとつ解決法はファイナライザーが呼ばれたとき、すでにデストラクタが呼ばれたかを検査する方法です。

(注)C++/CLIではデストラクタがコールされた場合、System::GC::SuppressFinalizeが自動的に呼び出されて、ファイナライザーは呼ばれなくなります。

#include <vector>
#include <assert.h>
using namespace System;
ref class C {
    std::vector<int>* v;
public:
    C() {
        v = new std::vector<int>;
    }
    ~C() {
        delete v;
        v = NULL;
    }
protected:
    !C() {
        assert(v==NULL);
    }
public:
    std::vector<int>* getList() {
        return v;
    }
};
void work()
{
    C^ c = gcnew C;
    std::vector<int>* v = c->getList();
    v->push_back(1);
    v->push_back(2);
    v->push_back(3);
    Console::WriteLine(v->size());
}
int main()
{
    work();
    GC::Collect();
    GC::WaitForPendingFinalizers();
    return 0;
}

このコードでは、~C()が呼ばれてないため、!C()のアサートに引っかかります。

しかしこうするとかならずdeleteを呼ばなければならなくなって.NETっぽくなくなります。一番いい方法はアンマネージリソースは外部に公開しないことだと思います。

#include <vector>
#include <assert.h>
using namespace System;
ref class C {
    std::vector<int>* v;
public:
    C() {
        v = new std::vector<int>;
    }
    ~C() {
        if (v)
        {
            delete v;
            v = NULL;
        }
    }
protected:
    !C() {
        if(v)
            delete v;
    }
public:
    void add(int i) {
        v->push_back(i);
    }
    property int Length
    {
        int get() { return v->size(); }
    }
};
int main()
{
    C^ c = gcnew C;
    c->add(1);
    c->add(2);
    c->add(3);
    Console::WriteLine(c->Length);
    return 0;
}

また、実際にはdeleteが複数回呼ばれたらどうするかとか、deleteされたのに他のメソッドが呼ばれたらどうするかとかも考えないとならないと思います。