Seaside Laboratory

Post

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; // エラー: 最後のループで確保されたヒープは解放されない
    }

    // 配列に保存
    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 がこんな仕様になっているのかしばらく理解できなかったが、自分もパーサーを書くときに getc した後に ungetc するコードを書いたりしているので、読む前に判定するのではなく、読んだ後に判定する方が処理コストが安いということなのだろう。