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 するコードを書くので、読む前に判定するのではなく、読んだ後に判定する方が処理コストが安いということなのだろう。