とんでもない出任せ

C++/CLIで詰まったところがあった。
注:この記事は口から出任せです。正しいかもしれませんが確証は有りません。直感で書いてます
まずはC++/CLIの説明から。C++/CLIってのはCLI (Microsoft .NET Framework 等;こもんらんげーじいんふらすとらくちゃーだった気がする) 用のC++拡張言語。こいつで作ったプログラムはCLIで動くので、当然ながらC#などと同じく、ガベージコレクタのある仮想マシン上で動作する。でも、文法はほとんどC++のまんま。更にC++/CLIC#より良いと言われるのは、RAIIが使いやすいからというのがある。RAIIってのはResource Acquisition Is Initializationの略で、要は特に「初期化」や「破棄」の手続きを踏まなくても、ちゃんといろいろなリソースの寿命を適切に指定できるというコト。更に要すれば、C++ のコンストラクタ・デストラクタが素晴らしいというそういう考え。普通、ガベージコレクタというのは (メモリという) リソース破棄をコンピュータに任せることで飯を食っているのであり、RAIIはしにくかったりする。が、C++/CLIは根がスタックベースなC++なので、RAIIも出来るゼ! ってのが偉い。説明終わり。疲れた。
で、マニュアルをたよりにこんなプログラムを書くわけ:

public ref class A {
public:
    A() {
        // 初期化処理
        b = gcnew B;
    }
    ~A() {
        // 破棄
        b->Uninit();
    }
    !A() {
        // デストラクタを呼ぶ
        ~A();
    }
private:
    B^ b;
};

public ref class というのはガベージコレクタ管理のクラスを作る宣言だと思いねえ。gcnew が new に対応してて、B^ というのは B* に対応してる。~A() はデストラクタ。!A() はファイナライザと呼ぶ。スタックに詰まれたりしたときは ~A() がちゃんと呼ばれるらしい (試してない) けど、A 自体も a = gcnew A とヒープに作ったときは、a がガベージコレクタにお掃除されてしまうときに !A() が呼ばれるという寸法。このとき ~A() は呼ばれないので、明示的にデストラクタを呼んでます。…なんかこのあたり自信がない…。
ところでこのプログラム、ダメ。場合によるけど、クラッシュしおる。ぬるぽとかじゃない。不正アクセスで「ガッ!」と落ちおる。何がおきてるのか。
問題は ~A() で、b の破棄処理を呼ぶために b->Uninit() してるのがとてつもなくヤバい。ちょっと考えれば分かることなんだけども、gcnew なオブジェクトのファイナライザから gcnew なオブジェクトは触れない
A への参照が切れて、まず A がガベージコレクタに掃除されて !A() が呼ばれ、続いて B への参照も切れる…という (おそらく上のプログラムの作者が思い描いていた) 流れなら問題は無いのだ。じゃあどういうときにクラッシュするかというと、こんなとき:

  • プログラム終了時、残ったオブジェクトが大掃除されるとき
  • A と B が循環参照して破棄されるとき

ふたつとも同じコトなので、後者を考えよう。循環参照してたら、どっちを先に破棄するか決めることは出来ない。ということは !A() で B を使い、!B() で A を使うことを認めたら、永遠に破棄できないことになるので、結局たとえメンバでも、ファイナライザから別のオブジェクトにはアクセスできないという制限を課すしかなくなる。なので、下手すると B の方が先に破棄されていて、ぬるぽにすらならず、ガッ! となる。
C++と同じ感覚でデストラクタを書いてるとこうなる。気を付けよう。


では、世の中の先駆者、つまりガベージコレクトなオブジェクト指向言語はどうやってRAIIを実現してるかみてみる。
じゃあC#

using (FileStream fs = new FileStream("Foo", FileMode.Read))
{
    // ...
}

(C++/CLI (4) 参照クラスとリソース管理 - イグトランスの頭の中より引用) using ブロックが終わると、FileStream のファイナライザが呼ばれる。

Boostの力を借りて、スマートポインタを手に入れたC++

{
    shared_ptr<FILE> file(fopen("Foo", "r"), fclose);
    // ...
}

(参考: http://cheesy.dip.jp/diary/archives/125) file がコピーされない限りは、} の時点でファイルがクローズされることが保証される。

Python (2.5)

with open('Foo', 'r') as f:
    # ...

(サイボウズ株式会社 より) with: ブロック終了時に f がクローズされることが保証される。

Ruby

open('Foo', 'r') do |f|
   # ...
end

(プログラミング言語 Ruby リファレンスマニュアル より) do end の終わりでクローズされると思いねえ。

Smalltalk (環境は Smalltalk/X) なら

'Foo' asFilename readingFileDo: [:stream |
    "..."].

[ ] ブロックの終わりでクローズ。*1

Smalltalkで言えば、本来はファイルストリームは閉じる処理、close がないといけない。
つまり、ガベージコレクタがあればいいというわけじゃあない。ガベージコレクタがあるからこそ、こういったイディオムを使って、リソースの寿命を余計に意識しないといけないと自戒する今日このごろ。

*1:案の定、Smalltalkの例が間違っていたので修正