OpenCVでカメラからのキャプチャを行うような事をC++/CLIでやろうとしています。で、まあリソース管理の為のコードを書くのは避けて通れない訳で、Disposeパターンに従って作ろうとしたら、public: virtual void Dispose(bool);
が通らない......予約されていると? と思って見てみたら、こんなページがMSDN Libraryにありました。
URIが日本語用(ja-jp)なのに翻訳されていないというのはとりあえず置いておいて、C#に慣れた頭で読んでみると驚きの連続でした。ついでに眠いせいか難しい。一通り読んだだけでは収拾がつかないので、デストラクタ(Wikipedia)なども見つつ重要そうな所を僕なりにちょっとまとめてみます。
C++/CLIにおけるデストラクタとファイナライザ
- デストラクタ
-
リソース(特にアンマネージドリソース)を積極的に解放する為にあるメソッド。リソースが要らなくなったらすぐ呼ばれるべきもので、スコープを抜ける時に自動的に呼ばれたり、プログラマが明示的に呼んだりする。
C++/CLIではデストラクタ構文(~classname())で表される。.NET FrameworkではIDisposable.Dispose()に相当する。C#はこいつを実装する事でデストラクタの機能を提供する。
- ファイナライザ
-
リソースの解放し忘れによる被害を最小限に抑える為にあるメソッド。ガベージコレクタが使う最後の砦。インスタンスが占有する領域を解放したくなった時、リークが無いかどうか調べて、もしあれば解放する(デストラクトする)為のもの。ガベージコレクタが解放したくならなければ(=メモリがいっぱいあれば)呼ばれない事もあり得る。
C++/CLIではファイナライザ構文(!classname())で表される。.NET FrameworkではObject.Finalize()に相当する。C#におけるファイナライザは、C#ではデストラクタと呼ばれているらしく(初めて知った気がするが気のせいか)、デストラクタ構文(~classname())で表される。
C#のファイナライザがデストラクタと呼ばれているのが混乱を招きそうですが、まあC#の~classname() <=> Finalize()と最初から覚えてしまえば良さそう。
マネージドリソースとアンマネージドリソース
色んな所で出てきますが、おさらい。
- マネージドリソース
- ガベージコレクタによって管理されるリソース。ガベージコレクタの管轄であるマネージドヒープに作られたオブジェクトの事。C++/CLIのref classオブジェクトとかC#のclassオブジェクトなんかもこれ。幸運にもガベージコレクタが管理できるという色が強い、と思う。
- アンマネージドリソース
- ガベージコレクタの管轄外にあるリソース。例えばCのmallocや標準C++のnewなどによって、ガベージコレクタが監視できないヒープに確保されたオブジェクトなどが含まれる。ファイルハンドルも代表例の一つ。ガベージコレクタは残念ながらこいつらを自動的に解放する事はできない。
ガベージコレクションにおけるファイナライゼーションの順番
これは、保証されません。マネージドリソースAが別のマネージドリソースBへの参照を持っていた場合などはAからファイナライズすればいいように思えますが、更にBがAへの参照を持つ事もあり得るので、そうなるとどちらからやればいいかは自明ではありません。勿論規則を明確にすればどのような場合でも一意に決められるでしょうが、想像してみると判るように、それを実現する為のコストは割と高い(外部環境の影響を受けないように追加情報(例えばインスタンス化時刻とか)を載せなければならない)ですし、第一人間がそんなもの把握しきれません。
となると、です。オブジェクトAのファイナライザが呼ばれた時には、オブジェクトBのファイナライザは既に呼ばれているかも知れません。ファイナライゼーションが済んでしまえばガベージコレクタはそのオブジェクトが占有していたメモリ領域を解放しますから、今までBへの正しい参照だったものは危険な(使ってはいけない)参照になります。そしてBのファイナライゼーションが既に行われたかどうかを知る術はAにはありません。ファイナライザでは、マネージドリソースへの参照を使ってはいけないのです。
もしBがアンマネージドリソースだった場合は逆で、むしろ責任を持って解放してやる必要があります。ガベージコレクタがアンマネージドリソースをファイナライズする事はありませんし、BがAによってのみ所有されていれば、AはBのファイナライゼーションが済んだかどうかは正確に把握できるはずです。
Disposeパターン
こうなってくると、デストラクタでは場合分けが必要になる事が判ります。明示的に(ファイナライゼーションよりも早い段階で)デストラクトされる場合と、ファイナライゼーションによってデストラクトされる場合です。この為に、C#などではboolをパラメータとするDisposeメソッドを用意する事が推奨されています。Disposeパターンと呼ばれるようです?
public class MyClass {
// disposingがtrueであればマネージドリソースのデストラクトも行う
// アンマネージドリソースのデストラクトはdisposingの値に依らず行う
// 派生クラスでやる内容変わるだろうからvirtualに
protected virtual void Dispose( bool disposing ) {
try {
if ( disposing ) {
// (マネージドリソースのデストラクト)
}
// (アンマネージドリソースのデストラクト)
}
finally {
// もし親がいれば
//base.Dispose( disposing );
}
}
// デストラクタ
// マネージド、アンマネージド両方とも解放
public void Dispose() {
// 明示的なデストラクション
this.Dispose( true );
System.GC.SuppressFinalize( this );
}
// ファイナライザ(デストラクタ構文)
// アンマネージドリソースのみ解放
~MyClass() {
// ガベージコレクタによるデストラクション
this.Dispose( false );
}
}
C++/CLIにおけるDisposeパターン等価コード
先程のコードを、C++/CLIで書くと次のようになります。
public ref class MyClass {
// デストラクタ
// マネージド、アンマネージド両方とも解放
~MyClass() {
// (マネージドリソースのデストラクト)
this->!MyClass();
}
// ファイナライザ
// アンマネージドリソースのみ解放
!MyClass() {
// (アンマネージドリソースのデストラクト)
}
};
美しい。これで等価だそうです。SuppressFinalize(Object)の所まで含めて。
このように書くと、デストラクタ構文の方は強いデストラクションをやっていて、ファイナライザ構文の方は弱いデストラクションをやっているようにも見えてきます。
まとめ
ま、上に書いた事がすべてではありますが、一応。
- C++/CLIでは、DisposeやFinalizeなどのメソッドは文字通り書く事は無い。デストラクタ構文、ファイナライザ構文を使う。
- ファイナライザでは他のマネージドリソースをデストラクトするべきではない。
- 明示的にデストラクタが呼ばれればマネージドリソースも含めてデストラクトするべきで(強いデストラクション)、
- ガベージコレクタのファイナライゼーションで初めてデストラクタが呼ばれるようなら、マネージドリソースはデストラクトすべきではない(弱いデストラクション)。
- C++/CLIでは、強いデストラクションをデストラクタ構文で、弱いデストラクションをファイナライザ構文で書く。
- つまり、デストラクタではマネージドリソースを解放した上でファイナライザを呼び出し、
- ファイナライザでアンマネージドリソースを解放する。
うん。読みながら理解しようとしていた時には混乱しかけていたけれど、まとめてみれば何て事無い、とても素直で綺麗な設計だと思います。さすがはリソース管理にうるさい(?)C++が前身の言語と言うべき? 気をつける事と言えば、C#ではファイナライザ(デストラクタ構文)がDispose(bool)を呼び出すのが普通なので、一見逆の事をやっているように見える事ですね。実際やっている事は同じだと。ふむ。
最近のコメント