Posts
ifstream の eof を理解しないとループは正しく回らない
ifstream のリファレンスを探していたら eof の問題を取り上げたページがやたら目についたので、記事を読んでみると以下のような単純なコードが正しく動作しないという話だった。
// ファイルを開く std::ifstream ifs; ifs.open( ... ); // ストリームが終わるまで while ( !ifs.eof() ) { // データ読み込み ifs.read( ... ); }
実際にプログラムを書いてデバッガーで確認してみると、読み込んだデータの量がひとつ多い。eof は read した後にデータが無いと分かった時点で true になるので、データがなくてもループに入り、最終要素が読み終わった後でも一度だけはループが実行されてしまう、というのが原因だった。
この手の書き方は Java の Iterator でも使われているくらいメジャーなイディオムなので、ifstream::eof だけ違った挙動をされても困ってしまう。
対応策として書かれていたのは、eof と read の実行順序を逆にするという方法。
// ストリームが終わるまで do { // 先に read を行うことで条件判定での eof が保証されるようになる ifs.read( ... ); } while ( !ifs.eof() );
ただ、この方法はメモリの動的確保が絡むと扱いが面倒になる。
// データをため込んでおくポインタ配列 (後で解放する) std::vector< char* > lpsDatas; // ストリームが終わるまで while ( true ) { // 確保したヒープにデータを読み込む char* lpsData = new char[256]; ifs.read( lpsData, sizeof( char ) * 256 ); // データがなかったら push_back する前に抜ける if ( ifs.eof() ) { break; // NG: 最後のループで確保されたヒープは解放されない } // 配列に保存 lpsDatas.push_back( lpsData ); }
一瞬「無条件にヒープを確保しているのが問題なのだからデータがあったときだけ確保すればいい。」と思ってしまうが、read しないことにはデータの有無が分からず、read するには読み込み先のヒープが必要となるというジレンマがある。
バッファを自動変数として定義すればメモリリークは回避できるが、バッファからのコピーが必要になるので処理コストは大きくなってしまう。
// データをため込んでおくポインタ配列 (後で解放する) std::vector< char* > lpsDatas; // データ読み込み用バッファ char sBuffer[256]; // ストリームが終わるまで while ( true ) { // バッファにデータを読み込む ifs.read( sBuffer, sizeof( char ) * 256 ); // データがなかったら push_back する前に抜ける if ( ifs.eof() ) { break; // OK: sBuffer は自動変数なのでメモリリークしない } // 確保したヒープにバッファの内容をコピー char* lpsData = new char[256]; std::copy( lpsData, sBuffer, 256 ); // 注意: 余計なコピーが発生する // 配列に保存 lpsDatas.push_back( lpsData ); }
どのやり方もいまいちなので、ifstream のインターフェイスに似せた代替ライブラリを自作することにした。C++ のストリーム入出力、C のファイル入出力、共にデータを読まずに EOF を検出する関数が無かったので、ファイルを開いたときに fseek でファイル末尾へ移動して終了オフセットを取得し、データを読み出す度に ftell の値と終了オフセットを比較して EOF を検出する方式。
何故 eof がこんな仕様になっているのかしばらく理解できなかったが、自分もパーサーを書くときに getc した後に ungetc するコードを書くので、読む前に判定するのではなく、読んだ後に判定する方が処理コストが安いということなのだろう。