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

595

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

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

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

MAK
источник
21
scanf(...) != EOFне будет работать в C, потому что scanfвозвращает количество полей, успешно проанализированных и назначенных. Правильное условие , scanf(...) < nгде nэто число полей в строке формата.
Бен Фойгт
5
@Ben Voigt, он вернет отрицательное число (которое EOF обычно определяется как таковое) в случае достижения EOF
Себастьян
19
@SebastianGodelet: На самом деле, он вернется, EOFесли конец файла встречается до первого преобразования поля (успешно или нет). Если между полями достигнут конец файла, он вернет количество полей, успешно преобразованных и сохраненных. Что делает сравнение EOFнеправильно.
Бен Фойгт
1
@SebastianGodelet: Нет, не совсем. Он ошибается, когда говорит, что «после цикла нет (простого) способа отличить правильный вход от неправильного». На самом деле это так же просто, как проверка .eof()после выхода из цикла.
Бен Фойгт
2
@Ben Да, для этого случая (читая простое int). Но можно легко придумать сценарий, в котором while(fail)цикл завершается как фактическим отказом, так и результатом. Подумайте, требуется ли вам 3 дюйма на итерацию (скажем, вы читаете точку xyz или что-то в этом роде), но в потоке ошибочно только два целых числа.
хитрый

Ответы:

544

Потому что 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
Xeo
источник
12
Стоит отметить, что if (! (InStream >> data) .eof ()) тоже ничего полезного не делает. Ошибка 1: Не будет введено условие, если после последнего фрагмента данных не было пробелов (последний элемент данных не будет обработан). Ошибка 2: Он войдет в условие, даже если чтение данных не удалось, если EOF не был достигнут (бесконечный цикл, обработка одних и тех же старых данных снова и снова).
Tronic
4
Я думаю, что стоит отметить, что этот ответ немного вводит в заблуждение. При извлечении ints или std::stringS или подобное, то бит EOF будет установлен , когда вы извлекаете одно право до конца и добыча попадает в конец. Вам не нужно читать снова. Причина, по которой он не устанавливается при чтении из файлов, заключается в том, что \nв конце есть дополнительное . Я рассказал об этом в другом ответе . Чтение chars - это другое дело, потому что оно извлекает только по одному за раз и не доходит до конца.
Джозеф Мэнсфилд
79
Основная проблема в том, что если мы не достигли EOF, это не означает, что следующее чтение будет успешным .
Джозеф Мэнсфилд
1
@sftrabbit: все верно, но не очень полезно ... даже если нет завершающего '\ n', разумно, чтобы другие конечные пробелы обрабатывались согласованно с другими пробелами в файле (то есть пропускались). Кроме того, тонкое последствие «когда вы извлекаете тот, что раньше» - это while (!eof())то, что не будет «работать» на ints или std::strings, когда входные данные полностью пусты, поэтому даже зная, что нет конечной \nзаботы, не требуется.
Тони Делрой
2
@TonyD Полностью согласен. Причина, по которой я это говорю, заключается в том, что я думаю, что большинство людей, когда они читают этот и аналогичные ответы, подумают, что если поток содержит "Hello"(без пробелов или \n) и a std::stringизвлечен, он извлечет буквы из Hв o, прекратит извлекать и тогда не устанавливайте бит EOF. Фактически, он установит бит EOF, потому что это был EOF, который остановил извлечение. Просто в надежде прояснить это для людей.
Джозеф Мэнсфилд
103

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

лукавый
источник
6
« Таким образом, после цикла нет (простого) способа отличить правильный вход от неправильного. » За исключением того, что в одном случае оба eofbitи failbitустановлены, в другом только failbitустановлены. Вам нужно проверить это только один раз после завершения цикла, а не на каждой итерации; он выйдет из цикла только один раз, поэтому вам нужно только проверить, почему он вышел из цикла один раз. while (in >> data)прекрасно работает для всех пустых потоков.
Джонатан Уэйкли
3
То, что вы говорите (и высказанное ранее замечание), состоит в том, что плохо отформатированный поток можно идентифицировать как !eof & failпрошедший цикл. Есть случаи, когда на это нельзя положиться. Смотрите выше комментарий ( goo.gl/9mXYX ). В любом случае, я не предлагаю - eofпроверять как всегда лучшую альтернативу. Я просто говорю, что это возможное и (в некоторых случаях более подходящий) способ сделать это, а не «безусловно , не так!» как это обычно утверждается здесь, в SO.
хитрый
2
« В качестве примера рассмотрим , как вы бы проверить на наличие ошибок , когда данные является структура с перегруженным оператором >> читает несколько полей сразу» - гораздо более простой случай , поддерживая вашу точку в stream >> my_intкотором поток содержит , например , «-»: eofbitи failbitявляются набор. Это хуже operator>>сценария, когда предоставленная пользователем перегрузка, по крайней мере, имеет возможность очистки, eofbitпрежде чем вернуться для поддержки while (s >> x)использования. В более общем смысле, в этом ответе может быть использована очистка - только финал, while( !(in>>ws).eof() )как правило, является надежным, и он похоронен в конце.
Тони Делрой
74

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

Наваз
источник
1
Помимо упомянутого «нежелательного результата» при выполнении работы с неопределенным значением n, программа может также попасть в бесконечный цикл , если сбойная операция потока не потребляет никакого ввода.
мастов
10

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

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

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

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

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

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

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

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

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


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

Мельпомена
источник
1
это самый точный ответ, хотя
начиная