Почему iostream :: eof внутри условия цикла (т.е. `while (! Stream.eof ())`) считается неправильным?
Я только что нашел комментарий в этом ответе, в котором говорится, что использование iostream::eof
в условии цикла «почти наверняка неправильно». Я обычно использую что-то вроде while(cin>>n)
- что, я думаю, неявно проверяет EOF.
Почему проверка eof явно используется while (!cin.eof())
неправильно?
Чем он отличается от использования scanf("...",...)!=EOF
в C (который я часто использую без проблем)?
Ответы
Потому что 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
Итог вверху: при правильной обработке пробелов 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)
несомненно, это более распространенная и краткая идиома, и она может быть предпочтительнее в простых сценариях (отдельные данные для каждого типа чтения).
Потому что, если программисты не пишут 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
если чтение было успешным, и цикл продолжается.
Другие ответы объяснили, почему логика неверна while (!stream.eof())
и как ее исправить. Я хочу сосредоточиться на другом:
почему проверка eof явно используется
iostream::eof
неправильно?
В общем, проверка eof
только неверна, потому что извлечение потока ( >>
) может завершиться неудачно, не попав в конец файла. Если у вас есть, например, int n; cin >> n;
и поток содержит hello
, значит, h
это недопустимая цифра, поэтому извлечение не удастся, не дойдя до конца ввода.
Эта проблема в сочетании с общей логической ошибкой проверки состояния потока перед попыткой чтения из него, что означает, что для N элементов ввода цикл будет выполняться N + 1 раз, приводит к следующим симптомам:
Если поток пуст, цикл выполняется один раз.
>>
завершится ошибкой (нет ввода для чтения), и все переменные, которые должны были быть установлены (с помощьюstream >> x
), фактически не инициализированы. Это приводит к обработке мусорных данных, которые могут проявляться в виде бессмысленных результатов (часто огромных чисел).(Если ваша стандартная библиотека соответствует C ++ 11, сейчас все немного по-другому: при ошибке
>>
теперь числовые переменные устанавливаются в значение0
вместо того, чтобы оставлять их неинициализированными (кромеchar
s).)Если поток не пуст, цикл будет запущен снова после последнего действительного ввода. Поскольку на последней итерации все
>>
операции завершаются неудачно, переменные, скорее всего, сохранят свое значение с предыдущей итерации. Это может проявляться как «последняя строка печатается дважды» или «последняя входная запись обрабатывается дважды».(Это должно проявляться немного иначе, чем в C ++ 11 (см. Выше): теперь вы получаете «фантомную запись» нулей вместо повторяющейся последней строки.)
Если поток содержит искаженные данные, но вы только проверяете их
.eof
, вы получаете бесконечный цикл.>>
не сможет извлечь какие-либо данные из потока, поэтому цикл вращается на месте, не доходя до конца.
Напомним: Решение испытать успех >>
самой операции, а не использовать отдельный .eof()
метод: while (stream >> n >> m) { ... }
так же , как в C вы проверить успешность scanf
самого вызова: while (scanf("%d%d", &n, &m) == 2) { ... }
.