思うだけで学ばない日記 2.0

思うだけで学ばない日記から移転しました☆!よろしくお願いします。

もうよい!std::istreamにはstd::ios::sitbitフラグがセットされるべき

std::istreamから行末まで読みたい場合、最もお手軽に済ませるにはstd::getline()を使うと良い。
動くサンプル:

#include <iostream>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <string>

/// ストリームの状態を表示します。(デバッグ用)
static void dump_stream_state(std::ios& stream)
{
    using namespace std;

    ios_base::iostate flags = stream.rdstate();
    bool eof = stream.eof();
    bool bad = stream.bad();
    bool good = stream.good();
    bool fail = stream.fail();
    cerr << "falgs=" << flags
         << ", good=" << good
         << ", bad=" << bad
         << ", fail=" << fail
         << ", eof=" << eof
         << endl;
}

// 行読み込み方法1: std::getline()使用
bool getline1(std::istream& stream, std::string& line)
{
    bool failed = !std::getline(stream, line);
    dump_stream_state(stream);
    return !failed;
}

int main(int argc, char* argv[])
{
    using namespace std;

    std::istringstream ist1("Hello World! This is a pen.\n");   // 改行有り
    std::istringstream ist2("The rain in Spain stays mainly in the plain."); // 改行無し
    std::string line;
    bool suc;
    cerr << boolalpha;

    cerr << "******* from istringstream (with LF) *******" << endl;
    suc = getline1(ist1, line);
    cerr << "line='" << line << "', result=" << suc << endl;

    cerr << "******* from cin (with LF) *******" << endl;
    suc = getline1(cin, line);
    cerr << "line='" << line << "', result=" << suc << endl;

    cerr << "******* from istringstream (without LF) *******" << endl;
    suc = getline1(ist2, line);
    cerr << "line='" << line << "', result=" << suc << endl;

    cerr << "******* from cin (without LF) *******" << endl;
    suc = getline1(cin, line);
    cerr << "line='" << line << "', result=" << suc << endl;

    return 0;
}

上記サンプルプログラムの標準入力に次のファイル内容をリダイレクトして実行してみる。

Hello World! This is a pen.
The rain in Spain stays mainly in the plain.

ただし2行目は、末尾の改行を無しとする。すると結果は次のようになる。

******* from istringstream (with LF) *******
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from cin (with LF) *******
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from istringstream (without LF) *******
falgs=1, good=false, bad=false, fail=false, eof=true
line='Hello World! This is a pen.', result=true
******* from cin (without LF) *******
falgs=1, good=false, bad=false, fail=false, eof=true
line='Hello World! This is a pen.', result=true

resultはture(成功)で、変数lineには行の内容が確かに入っている。
おk 計画通り

しかしstd::getline(stream, s)は、ストリームに改行が現れるまで際限なくsに読もうとするから、悪意のある入力によってハングアップさせられる危険性がある。そこで受け付ける行の長さに制限を設けよう。たとえば511文字まで受け付けるものとして、上記サンプルのgetline1()を次のように書き直せば良い。

// 行読み込み方法2: istream::getline()使用; バッファサイズ大
bool getline1(std::istream& stream, std::string& line)
{
    const int bufsz = 512;
    char buf[bufsz];
    bool failed = !stream.getline(buf, bufsz);
    dump_stream_state(stream);
    line = std::string(buf);
    return !failed;
}

結果。

******* from istringstream (with LF) *******
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from cin (with LF) *******
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from istringstream (without LF) *******
falgs=1, good=false, bad=false, fail=false, eof=true
line='The rain in Spain stays mainly in the plain.', result=true
******* from cin (without LF) *******
falgs=1, good=false, bad=false, fail=false, eof=true
line='The rain in Spain stays mainly in the plain.', result=true

まあおk
書き換え前のgetline1()と同じ結果になた!

しかし、正常な行の平均文字数が十数文字程度だとしたら、1行のために512文字分ものバッファを無条件に確保するのでは大名プログラマと後ろ指を指されてしまう。もっとバッファサイズを小さくして、行が長かったら分割して読み込むことにしよう。

// 行読み込み方法3: istream::getline()使用; バッファサイズ小
bool getline1(std::istream& stream, std::string& line)
{
    const int bufsz = 4;        // bufsz < 1行の長さ
    char buf[bufsz];
    line = "";
    while (stream.getline(buf, bufsz)) {
        dump_stream_state(stream);
        line += std::string(buf);
    }
    dump_stream_state(stream);
    return !stream.fail();
}

結果。

******* from istringstream (with LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
line='', result=false
******* from cin (with LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
line='', result=false
******* from istringstream (without LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
line='', result=false
******* from cin (without LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
line='', result=false

ニギャー; ことごとく失敗した*1…orz
どうもstd::istream::getline()は、行末を発見する前に書き込み先バッファが尽きた場合、std::ios::failbitフラグを立ててしまうっぽい

正しいコードは次のとおりである…

// 行読み込み方法4: istream::getline()使用; バッファサイズ小、対策版
bool getline1(std::istream& stream, std::string& line)
{
    const int bufsz = 16;        // bufsz < 1行の長さ
    char buf[bufsz];
    line = "";
    for (;;) {
        bool failed = !stream.getline(buf, bufsz);
        dump_stream_state(stream);
        if (failed) {
            line += std::string(buf);
            if (stream.bad()) return false;     // I/Oエラーが生じた場合
            stream.clear();
        } else {
            line += std::string(buf);
            break;
        }
    }
    return true;
}

…かどうかは保証の限りではないが(マテ;
これでとりあえずこれで次のように正しく動く↓

******* from istringstream (with LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from cin (with LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from istringstream (without LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=1, good=false, bad=false, fail=false, eof=true
line='The rain in Spain stays mainly in the plain.', result=true
******* from cin (without LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=1, good=false, bad=false, fail=false, eof=true
line='The rain in Spain stays mainly in the plain.', result=true

しかし「たかが」バッファオーバー対策(純然たるオンメモリ操作)のために複雑な条件分岐が必要となりline+=string(buf)を2つ書かされたり*2、あまつさえストリームのエラー回復の呪文であるところのstd::ios::clear()まで動員する羽目になったりとうんこ臭がぷんぷんするorz

まあ動くのなら我慢しよう、、しかしstd::cinをファイルからリダイレクトするのではなくて、コンソールからにした場合、かなり味わい深い挙動を見ることができる:

******* from istringstream (with LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from cin (with LF) *******
The rain in Spain stays mainly in the plain.
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=0, good=true, bad=false, fail=false, eof=false
line='Hello World! This is a pen.', result=true
******* from istringstream (without LF) *******
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=1, good=false, bad=false, fail=false, eof=true
line='The rain in Spain stays mainly in the plain.', result=true
******* from cin (without LF) *******
The rain in Spain stays mainly in the plain.^Z
falgs=2, good=false, bad=false, fail=true, eof=false
falgs=2, good=false, bad=false, fail=true, eof=false

falgs=0, good=true, bad=false, fail=false, eof=false
line='The rain in Spain stays mainly in the plain.?', result=true

これは、上述の入力ファイル内容("Hello World! ...")をコンソールから手入力し、2行目の末尾において[Ctrl+Z]、引き続き[RETURN]を押した。
最後の行の「?」には'\x0a'が入っている。
こうなる理由はもはや現代科学では解明できないのではないか*3

*1:result=falseで変数lineが空文字列。

*2:本番コードでは中間変数bool failedを外し隊。これはdump_stream_state()を呼ぶ関係で設けているにすぎない。

*3:Windows 7環境にてVS2008使用。めんどくさいので'\x0a'が入るところまでしか調べていない