Seaside Laboratory

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 );
}

バッファからのコピーが必要なので、処理コストが大きいのが難点…。

最終案

C++ のストリーム入出力、C のファイル入出力、共にデータを読まずに EOF を検出する関数が無かったので、以下のような処理を行う ifstream の代替ライブラリを自作することにした。

// ファイルを開く
this->m_stream = fopen( filename, mode );
// 終端へ移動
fseek( this->m_stream, 0, SEEK_END );
// 終端位置を取得
this->m_pos_end = ftell( this->m_stream );
// 先頭へ戻る
fseek( this->m_stream, 0, SEEK_SET );

// 現在位置と終端位置を比較
if ( ftell( this->m_stream ) == this->m_pos_end )
{
    // ifstream::eof 相当
}

何故 eof がこんな仕様になっているのかしばらく理解できなかったが、自分もパーサーを書くときに getc した後に ungetc するコードを書くので、読む前に判定するのではなく、読んだ後に判定する方が処理コストが安いということなのだろう。