Запутался в использовании `std :: istreambuf_iterator` - PullRequest
3 голосов
/ 11 ноября 2010

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

В конечном счете, моя цель состоит в том, чтобы иметь возможность перебирать поток с использованием istream_iterator<MyObject> ивставьте каждый объект в vector.Довольно стандартный, за исключением того, что у меня возникают проблемы с повторением istream_iterator до stop , когда он достигает конца потока.Сейчас он просто зацикливается навсегда, хотя вызовы 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() возвращает false, хотя istream::tellg() возвращает последний байт в файле, а istreambuf_iterator<char>(ifs) сравнивает true с istreambuf_iterator<char>(), что означает, что я определенно в конце потока.

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

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

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

Поэтому я попытался вручную установить бит перехода в моей подпрограмме 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 ]

6 голосов
/ 11 ноября 2010

Когда вы используете 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;
}

Edit

Я шел по коду в моем отладчике, и это то, что я нашел.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 не делает этого.

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

3 голосов
/ 12 ноября 2010

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

Ваш внутренний цикл & mdash; где вы используете istreambuf_iterator для извлечения символов из streambuf & mdash; использует функции более низкого уровня, такие как 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, что вызовет другие сюрпризы, такие как тот, что привел вас сюда.

2 голосов
/ 26 февраля 2011

В вашем 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.

1 голос
/ 11 ноября 2010

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

Я понял, что работает с этим:

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

    return is;
}
0 голосов
/ 11 ноября 2010

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

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

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

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

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

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

...