Запутался в использовании `std::istreambuf_iterator`

Я реализовал процедуру десериализации для объекта, используя << оператор потока. Сама процедура использует istreambuf_iterator<char> извлекать символы из потока один за другим, чтобы построить объект.

В конечном счете, моя цель состоит в том, чтобы иметь возможность перебирать поток, используя istream_iterator<MyObject> и вставьте каждый объект в vector, Довольно стандартный, но у меня проблемы с получением istream_iterator прекратить итерации, когда он достигает конца потока. Прямо сейчас, это просто зацикливается навсегда, даже если вызовы istream::tellg() указать, что я в конце файла.

Вот код для воспроизведения проблемы:

struct Foo
{
    Foo() { }    
    Foo(char a_, char b_) : a(a_), b(b_) { }

    char a;
    char b;
};

// Output stream operator
std::ostream& operator << (std::ostream& os, const Foo& f)
{
    os << f.a << f.b;
    return os;
}

// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    if (is.good()) 
    {
        std::istreambuf_iterator<char> it(is);
        std::istreambuf_iterator<char> end;

        if (it != end) {
            f.a = *it++;
            f.b = *it++;
        }
    }
    return is;
}

int main()
{
    {
        std::ofstream ofs("foo.txt");
        ofs << Foo('a', 'b') << Foo('c', 'd');
    }

    std::ifstream ifs("foo.txt");
    std::istream_iterator<Foo> it(ifs);
    std::istream_iterator<Foo> end;
    for (; it != end; ++it) cout << *it << endl; // iterates infinitely
}

Я знаю, что в этом тривиальном примере мне даже не нужен istreambuf_iterator, но я просто пытаюсь упростить проблему, так что, скорее всего, люди ответят на мой вопрос.

Так что проблема здесь в том, что хотя istreambuf_iterator достигает конца буфера потока, сам фактический поток не входит в EOF государство. Звонки в istream::eof() вернуть ложь, даже если istream::tellg() возвращает последний байт в файле, и istreambuf_iterator<char>(ifs) сравнивает истину с istreambuf_iterator<char>()Это означает, что я определенно в конце потока.

Я посмотрел на код библиотеки IOstreams, чтобы увидеть, как именно он определяет, является ли istream_iterator находится в конечной позиции, и в основном он проверяет, если istream::operator void*() const оценивает true, Эта функция библиотеки istream просто возвращает:

return this->fail() ? 0 : const_cast<basic_ios*>(this);

Другими словами, это возвращает 0 (false), если установлен failbit. Затем он сравнивает это значение с тем же значением в построенном по умолчанию экземпляре istream_iterator чтобы определить, если мы в конце.

Таким образом, я попытался вручную установить failbit в моем std::istream& operator >> (std::istream& is, Foo& f) рутина, когда istreambuf_iterator сравнивает истину с конечным итератором. Это сработало отлично, и правильно завершил цикл. Но сейчас я действительно растерялся. Кажется, что istream_iterator определенно проверяет std::ios::failbit чтобы обозначить условие "конец потока". Но разве это не то, что std::ios::eofbit для? я думал failbit был для условий ошибки, как, например, если основной файл fstream не может быть открыт или что-то.

Итак, зачем мне звонить istream::setstate(std::ios::failbit) чтобы завершить цикл?

5 ответов

Решение

Когда вы используете istreambuf_iterator, вы манипулируете базовым объектом streambuf объекта istream. Объект streambuf ничего не знает о своем владельце (объект istream), поэтому вызов функций для объекта streambuf не вносит изменений в объект istream. Вот почему флаги в объекте istream не устанавливаются при достижении eof.

Сделайте что-то вроде этого:

std::istream& operator >> (std::istream& is, Foo& f)
{
    is.read(&f.a, sizeof(f.a));
    is.read(&f.b, sizeof(f.b));
    return is;
}

редактировать

Я шел по коду в моем отладчике, и это то, что я нашел. istream_iterator имеет два внутренних элемента данных. Указатель на связанный объект istream и объект типа шаблона (в данном случае Foo). Когда вы вызываете ++ это, он вызывает эту функцию:

void _Getval()
{    // get a _Ty value if possible
    if (_Myistr != 0 && !(*_Myistr >> _Myval))
        _Myistr = 0;
}

_Myistr - указатель istream, а _Myval - объект Foo. Если вы посмотрите здесь:

!(*_Myistr >> _Myval)

Вот где это вызывает ваш оператор >> перегрузки. И это вызывает оператор! на возвращенном объекте istream. И, как вы можете видеть здесь, оператор! возвращает true, только если установлены failbit или badbit, eofbit не делает этого.

Итак, что будет дальше, если заданы биты с ошибками или биты, указатель istream получает значение NULL. И в следующий раз, когда вы сравниваете итератор с конечным итератором, он сравнивает указатель istream, который равен NULL для них обоих.

Ваш внешний цикл - где вы проверяете свои istream_iterator достиг своего конца - привязан к состоянию, хранящемуся в istream наследуется ios_base, Государство на istream представляет собой результат недавних операций извлечения, выполненных против istream само по себе, а не состояние его основной streambuf,

Ваш внутренний цикл - где вы используете istreambuf_iterator извлечь символы из streambuf - использует функции более низкого уровня, такие как basic_streambuf::sgetc() (за operator*) а также basic_streambuf::sbumpc() (за operator++). Ни одна из этих функций не устанавливает флаги состояния в качестве побочного эффекта, кроме продвигающейся второй basic_streambuf::gptr,

Ваш внутренний цикл работает нормально, но он реализован хитрым способом, упакованным как есть, и он нарушает договор std::basic_istream& operator>>(std::basic_istream&, T&), Если функция не может извлечь элемент, как предполагалось, она должна вызвать basic_ios::setstate(badbit) и, если он также столкнулся с концом потока при извлечении, он также должен вызвать basic_ios::setstate(eofbit), Ваша функция экстрактора не устанавливает ни один флаг, когда она не может извлечь Foo,

Я согласен с другим советом, чтобы избежать использования istreambuf_iterator для реализации оператора извлечения, предназначенного для работы на istream уровень. Вы заставляете себя делать дополнительную работу, чтобы поддерживать istream контракт, который вызовет другие сюрпризы вниз по течению, как тот, который привел вас сюда.

В вашем operator>> вы должны установить failbit каждый раз, когда вы не можете успешно прочитать Foo, Дополнительно вы должны установить eofbit каждый раз, когда вы обнаружите конец файла. Это может выглядеть так:

// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    if (is.good()) 
    {
        std::istreambuf_iterator<char> it(is);
        std::istreambuf_iterator<char> end;

        std::ios_base::iostate err = it == end ? (std::ios_base::eofbit |
                                                  std::ios_base::failbit) :
                                                 std::ios_base::goodbit;
        if (err == std::ios_base::goodbit) {
            char a = *it;
            if (++it != end)
            {
                char b = *it;
                if (++it == end)
                    err = std::ios_base::eofbit;
                f.a = a;
                f.b = b;
            }
            else
                err = std::ios_base::eofbit | std::ios_base::failbit;
        }
        if (err)
            is.setstate(err);
    }
    else
        is.setstate(std::ios_base::failbit);
    return is;
}

С помощью этого экстрактора, который устанавливает бит сбоя при сбое чтения и эофит при обнаружении файла, ваш драйвер работает как положено. Обратите внимание, что даже если ваш внешний if (is.good()) не удается, вам все равно нужно установить failbit, Ваш поток может быть !good() потому что только eofbit установлено.

Вы можете немного упростить вышесказанное, используя istream::sentry для внешнего испытания. Если sentry не удастся, он установит failbit для тебя:

// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    std::istream::sentry ok(is);
    if (ok) 
    {
        std::istreambuf_iterator<char> it(is);
        std::istreambuf_iterator<char> end;

        std::ios_base::iostate err = it == end ? (std::ios_base::eofbit |
                                                  std::ios_base::failbit) :
                                                 std::ios_base::goodbit;
        if (err == std::ios_base::goodbit) {
            char a = *it;
            if (++it != end)
            {
                char b = *it;
                if (++it == end)
                    err = std::ios_base::eofbit;
                f.a = a;
                f.b = b;
            }
            else
                err = std::ios_base::eofbit | std::ios_base::failbit;
        }
        if (err)
            is.setstate(err);
    }
    return is;
}

sentry также пропускает ведущие пробелы. Это может или не может быть то, что вы хотите. Если вы не хотите, чтобы часовой пропускал пропущенные начальные пробелы, вы можете создать его с помощью:

    std::istream::sentry ok(is, true);

Если sentry обнаруживает конец файла, пропуская первые пробелы, он установит оба failbit а также eofbit,

Похоже, что два набора потоковых итераторов взаимодействуют друг с другом:

Я получил это работает с этим:

// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    f.a = is.get();
    f.b = is.get();

    return is;
}

Я думаю, что ваше конечное состояние должно использовать .equal() метод, вместо использования оператора сравнения.

for (; !it.equal(end); ++it) cout << *it << endl;

Я обычно вижу, что это реализовано с помощью цикла while вместо цикла for:

while ( !it.equal(end)) {
    cout << *it++ << endl;
}

Я думаю, что эти два будут иметь тот же эффект, но (для меня) цикл while более понятен.

Примечание: у вас есть ряд других мест, где вы используете оператор сравнения, чтобы проверить, находится ли итератор в eof. Все это, вероятно, следует переключить на использование .equal(),

Другие вопросы по тегам