fstreamのデストラクタを追ってみた話

僕がC++をいいなぁと思う理由の一つに、RAIIが使えるという点がありました。 ただ最近RAIIに対して懐疑的に思えてきたので、そのことを書きたいと思います。

RAIIというのはオブジェクトのコンストラクタでファイルの初期化やロックの取得などリソースの獲得を行い、デストラクタでファイルのクローズやロックの開放など、リソースの開放を行うテクニックです。明示的にclose処理等を書く必要がないので、ファイルの閉じ忘れ、ロックの開放し忘れを防ぐことができます。 また処理の途中で例外の発生などの意図しない挙動が起きた際にもリソースが開放されることが保障されており、頑健なコードを書くことが可能となります。

Javaなどのガベージコレクションを持つ言語ではどのタイミングでオブジェクトが破棄されるかわらかないので、ファイルハンドルを開きっぱなしにしたり、ロックを持ち続けてしまう可能性もあってこのテクニックが使えないのですが、C++の場合、スコープから抜けた段階でデストラクタが呼ばれるということが保証されているのでそのような心配がいりません。

例えばC++標準ライブラリのfstreamオブジェクトは、デストラクタでファイルのcloseをすることが仕様にも明記されており、安心してcloseせずにいました。

いました。

ですが、つい最近Cで書かれているTokyo Cabinetのデータベースオブジェクトをラップするようなクラスを書いていて、fstreamと同じようにデストラクタでクローズする処理を書こうとした時のことでした。 エラー処理をどう書くべきかか悩みました。 研究用に書いていた程度のプログラミングではあまり気にせずにいたと思うのですが、仕事でも使えるようなというレベルのことを考えると、間違いなくクローズする関数の戻り値をチェックすべきです。

それでは、RAIIのテクニックを利用している時に、クローズ処理が失敗した時にはどうしたらいいでしょうか? 関数の返り値として失敗したという値を返したいですが・・・、デストラクタでは何も返すことができません。 それでは、例外を投げればいいのでしょうか?デストラクタで例外を投げるのは、使用される場所によって二重例外の発生、リソースが開放されない可能性が考えれますので、あまり推奨される行為ではありません。

気になったのでfstreamクラスのソースコードを追ってみました。 gcc-4.4.5, Linuxでは、実際に以下の手順でfclose(3)が呼ばれるようです。

basic_ostream::~basic_ostream()
==> basic_filebuf::~basic_filebuf()
====> baasic_filebuf::close()
======> __basic_file::close()
========> ::fclose()

ですので、実際にファイルクローズ時のエラー処理を行っているのはbasic_file::close()の時です。 その時の処理を引用してみます。

  __basic_file<char>*
  __basic_file<char>::close()
  {
    __basic_file* __ret = static_cast<__basic_file*>(NULL);
    if (this->is_open())
      {
        int __err = 0;
        if (_M_cfile_created)
          {
            errno = 0;
            do
              __err = fclose(_M_cfile);
            while (__err && errno == EINTR);
          }
        _M_cfile = 0;
        if (!__err)
          __ret = this;
      }
    return __ret;
  }

問題があった時はNULLを返していますね。 そしてこの問題が何か起きているぞという通知はbasic_filebufオブジェクトのデストラクタまで返ってますが、そこで華麗に無視されます。 ちなみにfcloseが失敗した時は、write(2),fflush(3), close(2)の全てのエラーの可能性があるため、書き込みに失敗してちゃんと保存できていない可能性があります。

fstreamを使ってちゃんとエラー処理を書く場合はスコープから外れた後にerrnoの値をチェックするか、もしくはデストラクタが呼ばれる前にcloseするしておくしかなさそうです。 ちなみに、事前にcloseを行なって失敗した場合はfstreamオブジェクトにfailビットが立ちますので、以下のように書けばエラー処理が可能です。

ofstream ofs("test.txt");

// 書き込み処理 begin

// 書き込み処理 end

ofs.close();
if(!ofs){
  //例外処理
}

99.9999%大丈夫なことが多いとは言え、残り0.0001%大丈夫かどうかを気にするのがプログラマですが、ちゃんとエラー処理を書く場合、毎回closeを事前に呼ばなければならないとしたら、RAIIにする意味がありません。保険、程度でしょうか。 C++のデストラクタでは副作用のあるコードしか記述できませんが、副作用のあるコードの大半はシステムコールが呼ばれたりするので、エラー処理が必要となります。 しかしデストラクタには呼び出し元にエラーがあったことを通知する手段が基本的に存在しません。

結局デストラクタで行うことができるのはコンストラクタで確保したメモリの自動的な開放程度となり、それならGCのある言語使った方がいいじゃないか……とかとか思う今日この頃でした。GCを使った方がパフォーマンスが高いとは、D言語FAQの弁です。

ちなみによく使われる手法ですが、pthread_mutex_lock, pthread_mutex_unlockをRAIIとして使うのは大丈夫な気がします。pthread_mutex_unlockはmutexオブジェクトが初期化されていない場合、もしくは呼び出しスレッドがmutexオブジェクトを所有していない(つまり不適切なオブジェクトが指定された時)しかエラーになりません。

なんか、ofstreamの実装を追っただけの話を書くつもりだったんですが、長文になってしまいました……。 みなさんofstreamのcloseとか毎回書いてるんですかね? まぁcloseに失敗しても、abortするぐらいしかできることがないかもしれませんが・・・少なくともエラーログは出力しておいてくれると問題発生時に助かると思います。