Какова реальная причина не использовать бит EOF в качестве условия извлечения потока?

Вдохновленный моим предыдущим вопросом

Общей ошибкой для новых программистов на C++ является чтение из файла с чем-то по строкам:

std::ifstream file("foo.txt");
std::string line;
while (!file.eof()) {
  file >> line;
  // Do something with line
}

Они часто сообщают, что последняя строка файла была прочитана дважды. Общее объяснение этой проблемы (которое я дал ранее) выглядит примерно так:

В результате извлечения будет установлен бит EOF в потоке, если вы попытаетесь извлечь конец файла, а не если ваше извлечение просто остановится в конце файла. file.eof() будет сообщать вам только о том, что предыдущее чтение попало в конец файла, а не в следующем. После того, как последняя строка была извлечена, бит EOF все еще не установлен, и итерация происходит еще раз. Однако на этой последней итерации извлечение завершается с ошибкой, а line все еще имеет тот же контент, что и раньше, т.е. Дублируется последняя строка.

Однако первое предложение этого объяснения неверно, поэтому объяснение того, что делает код, также неверно.

Определение форматированных входных функций (которое operator>>(std::string&)) определяет извлечение с использованием rdbuf()->sbumpc() или rdbuf()->sgetc() для получения входных символов. Он утверждает, что если любая из этих функций возвращает traits::eof(), тогда бит EOF устанавливается:

Если rdbuf()->sbumpc() или rdbuf()->sgetc() возвращает traits::eof(), тогда входная функция, за исключением случаев, когда это явно указано иначе, завершает свои действия и делает setstate(eofbit), что может бросить ios_base::failure (27.5.5.4), прежде чем возвращать.

Мы можем видеть это с помощью простого примера, который использует std::stringstream, а не файл (они оба являются входными потоками и ведут себя одинаково при извлечении):

int main(int argc, const char* argv[])
{
  std::stringstream ss("hello");
  std::string result;
  ss >> result;
  std::cout << ss.eof() << std::endl; // Outputs 1
  return 0;
}

Здесь ясно, что одно извлечения получает hello из строки и устанавливает бит EOF в 1.

Так что не так с объяснением? Что отличает файлы, вызывающие !file.eof(), чтобы дублировать последнюю строку? Какова реальная причина, по которой мы не должны использовать !file.eof() в качестве нашего условия извлечения?

Ответ 1

Да, извлечение из входного потока будет устанавливать бит EOF, если извлечения останавливается в конце файла, как показано в примере std::stringstream. Если бы это было так просто, цикл с !file.eof() в качестве его условия отлично работал бы на файле, например:

hello
world

Второе извлечение ест world, останавливается в конце файла и, следовательно, устанавливает бит EOF. Следующая итерация не произойдет.

Однако у многих текстовых редакторов есть грязный секрет. Они лгут вам, когда вы сохраняете текстовый файл так же просто. То, что они не говорят вам, это то, что в конце файла есть скрытый \n. Каждая строка в файле заканчивается на \n, включая последнюю. Таким образом, файл содержит:

hello\nworld\n

Это то, что заставляет последнюю строку дублировать при использовании !file.eof() в качестве условия. Теперь, когда мы это знаем, мы видим, что второе извлечение будет world останавливаться на \n и не устанавливать бит EOF (потому что мы еще не получили его). Цикл будет повторяться в третий раз, но следующее извлечение завершится неудачно, потому что он не находит строку для извлечения, только пробел. Строка остается с прежним значением, которое все еще висит вокруг, и поэтому мы получаем дублируемую строку.

Вы не испытываете этого с помощью std::stringstream, потому что то, что вы вставляете в поток, - это именно то, что вы получаете. Там нет \n в конце std::stringstream ss("hello"), в отличие от файла. Если вы должны были сделать std::stringstream ss("hello\n"), вы столкнетесь с такой же проблемой с дублирующейся строкой.

Итак, мы можем видеть, что мы никогда не должны использовать !file.eof() как условие при извлечении из текстового файла - но что здесь представляет собой настоящая проблема? Почему мы никогда не должны использовать это как наше условие, независимо от того, извлекаем ли мы из файла или нет?

Реальная проблема заключается в том, что eof() не дает представления о том, произойдет ли следующее чтение или нет. В приведенном выше случае мы видели, что даже если eof() равно 0, следующее извлечение завершилось неудачно, потому что не было строки для извлечения. Такая же ситуация возникла бы, если бы мы не связали поток файлов с каким-либо файлом или поток не был пустым. Бит EOF не будет установлен, но читать нечего. Мы не можем просто слепо идти вперед и извлекать из файла только потому, что eof() не установлен.

Использование while (std::getline(...)) и связанных с ним условий отлично работает, так как перед началом извлечения форматированная входная функция проверяет, установлены ли какие-либо из битов bad, fail или EOF. Если какой-либо из них, он немедленно заканчивается, устанавливая бит сбоя в этом процессе. Он также потерпит неудачу, если он найдет конец файла, прежде чем он найдет то, что он хочет извлечь, установив как биты eof, так и fail.


Примечание. Вы можете сохранить файл без дополнительного \n в vim, если вы выполняете :set noeol и :set binary перед сохранением.

Ответ 2

У вашего вопроса есть некоторые фиктивные концепции. Вы даете объяснение:

"При извлечении будет только установлен бит EOF в потоке, если вы попытаетесь извлечь конец файла, а не если ваше извлечение просто остановится в конце файла."

Затем утверждают, что "неправильно, и поэтому объяснение того, что делает код, также неверно".

Собственно, это правильно. Давайте посмотрим на пример....

При чтении в std::string...

std::istringsteam iss('abc\n');
std::string my_string;
iss >> my_string;

... по умолчанию, и как в вашем вопросе operator>> читается символ, пока он не находит пробел или EOF. Итак:

  • чтение из 'abc\n' → после того, как встречается '\n', он не пытается извлечь конец файла, а просто "останавливается в [EOF]" и eof() won ' t return true,
  • чтение из 'abc' вместо этого → попытка извлечь конец файла, который обнаруживает конец содержимого string, поэтому eof() вернет true.

Аналогично, синтаксический анализ '123' в int устанавливает eof(), потому что синтаксический анализ не знает, будет ли другая цифра, и попытается продолжить чтение, нажав eof(). Разбор '123 ' на int не будет установлен eof().

Важней, разбор 'a' в char не будет устанавливать eof(), потому что конечный пробел не нужен, чтобы знать, что синтаксический анализ завершен - после считывания символа никакая попытка не производится для поиска другого символа и eof() не встречается. (Конечно, дальнейший синтаксический анализ из одного и того же потока достигает eof).

Очистить [для stringstream "hello" → std::string], что одно извлечения получает привет от строки и устанавливает бит EOF в 1. Так что же не так с объяснением? Что отличает файлы, вызывающие! File.eof(), чтобы вызвать дублирование последней строки? Какова реальная причина, по которой мы не должны использовать file.eof() в качестве нашего условия извлечения?

Причина такова, как указано выше: файлы, как правило, заканчиваются символом "\n", а когда они означают getline или >> std::string, возвращают последний токен без пропусков, не требуя "попытки извлечь end-of-file" (для использования вашей фразы).