Запутался в использовании `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()
,