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

Apr 09 2011

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

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

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

Ответы

556 Xeo Apr 09 2011 at 19:58

Потому что 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
104 sly Nov 24 2012 at 06:30

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

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

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


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

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

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()проверяется, что если есть что читать, это то, что нужно. Его цель - не просто терминатор цикла while.

Пока все хорошо, но что произойдет, если в потоке есть конечный пробел - что звучит как основная проблема в 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)несомненно, это более распространенная и краткая идиома, и она может быть предпочтительнее в простых сценариях (отдельные данные для каждого типа чтения).

75 Nawaz Apr 09 2011 at 19:59

Потому что, если программисты не пишут 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если чтение было успешным, и цикл продолжается.

10 melpomene May 04 2019 at 16:52

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

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

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

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

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

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

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

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

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


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