Почему iostream :: eof внутри условия цикла (то есть 'while (! Stream.eof())') считается неправильным?

Я только что нашел комментарий в этом ответе о том, что использование iostream::eof в условии цикла "почти наверняка неправильно". Я обычно использую что-то вроде while(cin>>n) - которое, я думаю, неявно проверяет EOF.

Почему проверка eof явно использует while (!cin.eof()) неправильно?

Чем он отличается от использования scanf("...",...)!=EOF в C (который я часто использую без проблем)?

Ответы

Ответ 1

Потому что iostream::eof возвращает только true после прочтения конца потока. Он не указывает, что следующее чтение будет концом потока.

Рассмотрим это (и предположим, что следующее чтение будет в конце потока):

while(!inStream.eof()){
  int data;
  // yay, not end of stream yet, now read ...
  inStream >> data;
  // oh crap, now we read the end and *only* now the eof bit will be set (as well as the fail bit)
  // do stuff with (now uninitialized) data
}

Против этого:

int data;
while(inStream >> data){
  // when we land here, we can be sure that the read was successful.
  // if it wasn't, the returned stream from operator>> would be converted to false
  // and the loop wouldn't even be entered
  // do stuff with correctly initialized data (hopefully)
}

И на ваш второй вопрос: Потому что

if(scanf("...",...)!=EOF)

совпадает с

if(!(inStream >> data).eof())

и не, как

if(!inStream.eof())
    inFile >> data

Ответ 2

Верх нижней части: При правильной обработке белого пространства, как eof можно использовать (и даже быть более надежным, чем fail() для проверки ошибок):

while( !(in>>std::ws).eof() ) {  
   int data;
   in >> data;
   if ( in.fail() ) /* handle with break or throw */; 
   // now use data
}    

(Спасибо Tony D за предложение выделить ответ. См. его комментарий ниже для примера, почему это более надежное.)


Основной аргумент против использования eof(), похоже, не содержит важной тонкости в отношении роли белого пространства. Мое предложение состоит в том, что проверка eof() явно не только не всегда "всегда ошибочна", что, по-видимому, является основным мнением в этом и подобном потоке SO, но при правильном обращении с белым пространством он обеспечивает чище и более надежная обработка ошибок, и это всегда правильное решение (хотя и не обязательно tersest).

Подводя итог тому, что предлагается, поскольку "правильное" окончание и порядок чтения следующие:

int data;
while(in >> data) {  /* ... */ }

// which is equivalent to 
while( !(in >> data).fail() )  {  /* ... */ }

Ошибка, вызванная попыткой чтения за пределами eof, принимается за условие завершения. Это означает, что нет простого способа отличить успешный поток и тот, который действительно терпит неудачу по другим причинам, кроме eof. Возьмите следующие потоки:

  • 1 2 3 4 5<eof>
  • 1 2 a 3 4 5<eof>
  • a<eof>

while(in>>data) завершается с набором failbit для всех трех входных данных. В первом и третьем также устанавливается eofbit. Таким образом, за циклом нужна очень уродливая дополнительная логика, чтобы отличить правильный вход (1-й) от неправильных (2-й и 3-й).

Принимая во внимание следующее:

while( !in.eof() ) 
{  
   int data;
   in >> data;
   if ( in.fail() ) /* handle with break or throw */; 
   // now use data
}    

Здесь in.fail() проверяет, что до тех пор, пока есть что прочитать, он правильный. Цель - не просто терминатор цикла.

До сих пор так хорошо, но что происходит, если в потоке есть конечное пространство - что звучит как основная проблема с eof() как терминатором?

Нам не нужно сдавать свою обработку ошибок; просто съешь белое пространство:

while( !in.eof() ) 
{  
   int data;
   in >> data >> ws; // eat whitespace with std::ws
   if ( in.fail() ) /* handle with break or throw */; 
   // now use data
}

std::ws пропускает любое потенциальное (ноль или более) конечное пространство в потоке при установке eofbit и не failbit. Таким образом, in.fail() работает так, как ожидалось, пока есть хотя бы одна информация для чтения. Если все пустые потоки также приемлемы, то правильная форма:

while( !(in>>ws).eof() ) 
{  
   int data;
   in >> data; 
   if ( in.fail() ) /* handle with break or throw */; 
   /* this will never fire if the eof is reached cleanly */
   // now use data
}

Сводка: Правильно построенный while(!eof) не только возможен, но и ошибочен, но позволяет локализовать данные в пределах области действия и обеспечивает более чистое разделение проверки ошибок от бизнеса, как обычно. При этом while(!fail) является, несомненно, более распространенной и краткой идиомой, и может быть предпочтительнее в простых (одиночных данных для типа чтения).

Ответ 3

Потому что, если программисты не пишут while(stream >> n), они, возможно, напишут это:

while(!stream.eof())
{
    stream >> n;
    //some work on n;
}

Здесь проблема заключается в том, что вы не можете выполнить some work on n, не проверив, действительно ли чтение потока было успешным, потому что если оно не увенчалось успехом, ваш some work on n приведет к нежелательному результату.

Все дело в том, что eofbit, badbit или failbit устанавливаются после попытки чтения из потока. Так что если stream >> n не удается, тогда eofbit, badbit или failbit устанавливается немедленно, поэтому его более идиоматично, если вы пишете while (stream >> n), потому что возвращенный объект stream преобразуется в false, если был некоторый сбой при чтении из потока и, следовательно цикл останавливается. И он преобразуется в true, если чтение было успешным, и цикл продолжается.

Ответ 4

Другие ответы объяснили, почему логика неверна в while (!stream.eof()) и как ее исправить. Я хочу сосредоточиться на чем-то другом:

почему проверка eof явно использует iostream::eof неправильно?

В общих чертах, проверка только для eof является неправильной, поскольку извлечение потока (>>) может завершиться неудачно, не достигнув конца файла. Если у вас есть, например, int n; cin >> n; и поток содержит hello, тогда h не является допустимой цифрой, поэтому извлечение завершится неудачей, не достигнув конца ввода.

Эта проблема в сочетании с общей логической ошибкой проверки состояния потока перед попыткой чтения из него, что означает, что для N входных элементов цикл будет выполняться N + 1 раз, приводит к следующим симптомам:

  • Если поток пуст, цикл будет запущен один раз. >> потерпит неудачу (нет входных данных для чтения), и все переменные, которые должны были быть установлены (согласно stream >> x), фактически неинициализированы. Это приводит к обработке мусорных данных, что может привести к бессмысленным результатам (часто огромным).

    (Если ваша стандартная библиотека соответствует С++ 11, теперь все немного по-другому: сбойный >> теперь устанавливает числовые переменные в 0 вместо того, чтобы оставлять их неинициализированными (за исключением char).)

  • Если поток не пустой, цикл будет запущен снова после последнего допустимого ввода. Поскольку в последней итерации все операции >> завершаются неудачно, переменные, скорее всего, сохранят свое значение из предыдущей итерации. Это может проявляться как "последняя строка печатается дважды" или "последняя входная запись обрабатывается дважды".

    (Это должно проявляться немного иначе, чем в С++ 11 (см. выше): теперь вы получаете "фантомную запись" с нулями вместо повторяющейся последней строки.)

  • Если поток содержит искаженные данные, но вы проверяете только .eof, вы получаете бесконечный цикл. >> не сможет извлечь какие-либо данные из потока, поэтому цикл вращается, даже не достигнув конца.


Напомним: решение состоит в том, чтобы проверить успешность самой операции >>, а не использовать отдельный метод .eof(): while (stream >> n >> m) { ... }, так же как в C вы проверяете успешность самого вызова scanf: while (scanf("%d%d", &n, &m) == 2) { ... }.

Ответ 5

1 while (!read.fail()) {
2     cout << ch;
3     read.get(ch);
4 }

Если вы используете строку 2 в 3 и строку 3 в 2, вы дважды печатаете ch. Итак, cout перед чтением.