Dlaczego iostream :: eof wewnątrz warunku pętli (tj. `While (! Stream.eof ())`) jest uważany za niewłaściwy?

595

Właśnie znalazłem komentarz w tej odpowiedzi, który mówi, że używanie iostream::eofw pętli jest „prawie na pewno złe”. Zasadniczo używam czegoś takiego while(cin>>n)- co domyślnie sprawdza EOF.

Dlaczego jawne sprawdzanie eofa jest while (!cin.eof())nieprawidłowe?

Czym różni się od używania scanf("...",...)!=EOFw C (z którego często korzystam bez problemów)?

MAK
źródło
21
scanf(...) != EOFteż nie będzie działać w C, ponieważ scanfzwraca liczbę pól pomyślnie przeanalizowanych i przypisanych. Prawidłowy stan jest scanf(...) < ngdzie njest liczba pól w ciągu formatu.
Ben Voigt,
5
@Ben Voigt, zwróci liczbę ujemną (którą EOF zwykle określa się jako taką) w przypadku osiągnięcia EOF
Sebastian
19
@SebastianGodelet: W rzeczywistości zwróci, EOFjeśli napotka koniec pliku przed pierwszą konwersją pola (udaną lub nie). Jeśli osiągnięty zostanie koniec pliku między polami, zwróci liczbę pól pomyślnie przekonwertowanych i zapisanych. Co sprawia, że ​​porównanie jest EOFzłe.
Ben Voigt
1
@SebastianGodelet: Nie, nie bardzo. Myli się, gdy mówi, że „za pętlą nie ma (łatwego) sposobu na odróżnienie właściwego wejścia od niewłaściwego”. W rzeczywistości jest to tak proste, jak sprawdzenie .eof()po wyjściu z pętli.
Ben Voigt
2
@Ben Tak, w tym przypadku (czytanie prostej int). Ale łatwo można wymyślić scenariusz, w którym while(fail)pętla kończy się zarówno faktyczną awarią, jak i eofem. Zastanów się, czy potrzebujesz 3 ints na iterację (powiedz, że czytasz punkt xyz lub coś takiego), ale błędnie w strumieniu są tylko dwie inty.
chytry

Odpowiedzi:

544

Ponieważ iostream::eofwróci dopiero true po przeczytaniu końca strumienia. To nie nie wskazuje, że następny odczyt będzie koniec strumienia.

Zastanów się (i załóż, że następny odczyt będzie na końcu strumienia):

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
}

Przeciwko temu:

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)
}

I na twoje drugie pytanie: Ponieważ

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

jest taki sam jak

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

i nie to samo co

if(!inStream.eof())
    inFile >> data
Xeo
źródło
12
Warto wspomnieć, że if (! (InStream >> data) .eof ()) też nie robi nic użytecznego. Błąd 1: Nie wejdzie w warunek, jeśli po ostatnim fragmencie danych nie było białych znaków (ostatni układ odniesienia nie zostanie przetworzony). Fallacy 2: Wejdzie w stan, nawet jeśli odczyt danych nie powiedzie się, dopóki EOF nie zostanie osiągnięty (nieskończona pętla, przetwarzanie tych samych starych danych w kółko).
Tronic
4
Myślę, że warto zauważyć, że ta odpowiedź jest nieco myląca. Podczas wyodrębniania ints lub std::strings lub podobnych bit EOF jest ustawiany, gdy wyodrębnisz go tuż przed końcem, a ekstrakcja dojdzie do końca. Nie musisz czytać ponownie. Powodem, dla którego nie ustawia się podczas odczytu z plików, jest to, że \nna końcu jest dodatkowy . Omówiłem to w innej odpowiedzi . Czytanie chars to inna sprawa, ponieważ wyodrębnia ona tylko jeden na raz i nie osiąga końca.
Joseph Mansfield,
79
Główny problem polega na tym, że to, że nie dotarliśmy do EOF, nie oznacza, że ​​następny odczyt się powiedzie .
Joseph Mansfield,
1
@ sftrabbit: wszystkie prawdziwe, ale niezbyt przydatne ... nawet jeśli nie ma końcowych znaków \ ​​n 'rozsądne jest, aby inne końcowe białe znaki były obsługiwane spójnie z innymi białymi znakami w całym pliku (tzn. pomijane). Ponadto, subtelna konsekwencją „kiedy wyodrębnić jedno prawo przed” jest to, że while (!eof())nie będzie „praca” na ints lub std::strings, gdy wejście jest całkowicie pusty, więc nawet nie wiedząc, że nie ma spływu \nopieka jest potrzebna.
Tony Delroy
2
@TonyD Całkowicie się zgadzam. Powodem, dla którego to mówię, jest to, że myślę, że większość ludzi, kiedy to czyta i podobne odpowiedzi będą myśleć, że jeśli strumień zawiera "Hello"(bez końcowych białych znaków lub \n) i std::stringjest wyodrębniany, wyodrębni litery od Hdo o, przestanie wyodrębniać i wtedy nie ustawiaj bitu EOF. W rzeczywistości ustawiłby bit EOF, ponieważ to EOF zatrzymał ekstrakcję. Mam nadzieję, że wyjaśnię to ludziom.
Joseph Mansfield,
103

Konkluzja: Przy prawidłowym obchodzeniu się z białą przestrzenią można zastosować następujące metody eof(a nawet być bardziej niezawodne niż fail()przy sprawdzaniu błędów):

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

( Dzięki Tony D za sugestię podkreślenia odpowiedzi. Zobacz jego komentarz poniżej, aby dowiedzieć się, dlaczego jest to bardziej niezawodne ).


Głównym argumentem przeciwko używaniu eof()wydaje się być brak ważnej subtelności na temat roli białej przestrzeni. Moja propozycja polega na tym, że eof()jawne sprawdzanie nie tylko „nie zawsze jest błędne ” - co wydaje się być nadrzędną opinią w tym i podobnych wątkach SO - ale także przy prawidłowej obsłudze białych znaków zapewnia czystsze i bardziej niezawodne obsługa błędów i jest zawsze poprawnym rozwiązaniem (choć niekoniecznie najkrótszym).

Podsumowując, co sugeruje się jako „prawidłowe” zakończenie i kolejność odczytu, należy:

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

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

Niepowodzenie spowodowane próbą odczytu poza eof jest traktowane jako warunek zakończenia. Oznacza to, że nie ma łatwego sposobu na odróżnienie udanego strumienia od takiego, który naprawdę zawodzi z powodów innych niż eof. Weź następujące strumienie:

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

while(in>>data)kończy się zestawem failbitdla wszystkich trzech wejść. W pierwszym i trzecimeofbit ustawiono również. Tak więc poza pętlą potrzebna jest bardzo brzydka dodatkowa logika, aby odróżnić prawidłowe dane wejściowe (1.) od niewłaściwych (2. i 3.).

Podczas gdy weź następujące:

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

Tutaj, in.fail() sprawdza, czy dopóki coś jest do przeczytania, jest to poprawne. Jego celem nie jest zwykły terminator pętli while.

Jak dotąd tak dobrze, ale co się stanie, jeśli w strumieniu pozostanie wolna przestrzeń - co wydaje się być głównym problemem eof() terminatorowi?

Nie musimy rezygnować z obsługi błędów; po prostu zjedz białą przestrzeń:

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::wspomija wszelkie potencjalne (zero lub więcej) końcowe miejsce w strumieniu podczas ustawiania eofbit, a niefailbit . in.fail()Działa więc zgodnie z oczekiwaniami, o ile istnieje co najmniej jedno dane do odczytania. Jeśli dopuszczalne są również całkowicie puste strumienie, wówczas poprawna forma to:

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
}

Podsumowanie: Prawidłowo skonstruowany while(!eof)jest nie tylko możliwy i niepoprawny, ale umożliwia lokalizację danych w zakresie i zapewnia bardziej czyste oddzielenie sprawdzania błędów od działalności w zwykły sposób. To powiedziawszy, while(!fail)jest bez wątpienia bardziej powszechnym i zwięzłym idiomem i może być preferowane w prostych scenariuszach (pojedyncze dane na typ odczytu).

chytry
źródło
6
Tak więc obok pętli nie ma (łatwy) sposób odróżnić prawidłowe wejście od niewłaściwej jeden. ” Poza tym, że w jednym przypadku oba eofbiti failbitsą ustawione, w drugiej tylko failbitjest ustawiony. Trzeba tylko przetestować to raz po zakończeniu pętli, nie przy każdej iteracji; opuści pętlę tylko raz, więc musisz tylko sprawdzić, dlaczego raz opuścił pętlę. while (in >> data)działa dobrze dla wszystkich pustych strumieni.
Jonathan Wakely
3
To, co mówisz (i wspomniano wcześniej), że źle sformatowany strumień można zidentyfikować jako !eof & failprzeszłą pętlę. Są przypadki, w których nie można na tym polegać. Zobacz powyższy komentarz ( goo.gl/9mXYX ). Tak czy inaczej, nie proponuję eof-check jako zawsze lepszej alternatywy. Mówię tylko, to jest to możliwy i (w niektórych przypadkach bardziej odpowiedni) sposób, aby to zrobić, zamiast „z pewnością źle!” jak zwykle twierdzi się tutaj w SO.
chytry
2
„Jako przykład zastanów się, jak sprawdzić błędy, gdy dane są strukturą z przeciążonym operatorem >> czytając wiele pól jednocześnie” - o wiele prostszym przypadkiem wspierającym twój punkt jest to, stream >> my_intgdzie strumień zawiera np. „-”: eofbiti failbitsą zestaw. Jest to gorsze niż operator>>scenariusz, w którym przeciążenie dostarczone przez użytkownika ma przynajmniej opcję wyczyszczenia eofbitprzed powrotem, aby pomóc w while (s >> x)użyciu. Mówiąc bardziej ogólnie, ta odpowiedź mogłaby przydać się w czyszczeniu - tylko finał while( !(in>>ws).eof() )jest ogólnie solidny i jest zakopany na końcu.
Tony Delroy
74

Ponieważ jeśli programiści nie piszą while(stream >> n), prawdopodobnie piszą to:

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

Problem polega na tym, że nie można obejść się some work on nbez uprzedniego sprawdzenia, czy odczyt strumienia się powiódł, ponieważ jeśli się nie powiedzie, some work on nwynik byłby niepożądany.

Istotą jest to, że eofbit, badbitalbo failbitpo podejmowana jest próba odczytu z potoku. Jeśli więc stream >> nzawiedzie eofbit,badbit lub failbitjest ustawiane natychmiast, więc jest bardziej idiomatyczne, jeśli piszesz while (stream >> n), ponieważ zwracany obiekt streamkonwertuje na, falsejeśli wystąpił błąd odczytu ze strumienia i w konsekwencji pętla zatrzymuje się. Konwertuje się na to, trueczy odczyt się powiódł i pętla trwa.

Nawaz
źródło
1
Oprócz wspomnianego „niepożądanego wyniku” przy wykonywaniu pracy na nieokreślonej wartości n, program może również wpaść w nieskończoną pętlę , jeśli nieudana operacja strumieniowa nie zużywa żadnych danych wejściowych.
mastov
10

Inne odpowiedzi wyjaśniły, dlaczego logika jest błędna while (!stream.eof())i jak to naprawić. Chcę się skupić na czymś innym:

dlaczego jawne sprawdzanie eofa jest iostream::eofnieprawidłowe?

Ogólnie rzecz biorąc, sprawdzanie eof tylko jest niepoprawne, ponieważ ekstrakcja strumienia ( >>) może się nie powieść bez uderzania w koniec pliku. Jeśli masz np. int n; cin >> n;A strumień zawiera hello, hto nie jest prawidłową cyfrą, więc wyodrębnienie zakończy się niepowodzeniem bez dotarcia do końca danych wejściowych.

Ten problem, w połączeniu z ogólnym błędem logicznym sprawdzania stanu strumienia przed próbą odczytu z niego, co oznacza, że ​​dla N elementów wejściowych pętla będzie działać N + 1 razy, prowadzi do następujących symptomów:

  • Jeśli strumień jest pusty, pętla uruchomi się raz. >>zawiedzie (nie ma danych wejściowych do odczytania) i wszystkie zmienne, które miały zostać ustawione (wgstream >> x ), są w rzeczywistości niezainicjowane. Prowadzi to do przetwarzania śmieciowych danych, co może przejawiać się jako nonsensowne wyniki (często ogromne liczby).

    (Jeśli twoja standardowa biblioteka jest zgodna z C ++ 11, sprawy wyglądają teraz trochę inaczej: błąd nie >>ustawia teraz zmiennych numerycznych 0zamiast pozostawiać je niezainicjowane (z wyjątkiem chars).)

  • Jeśli strumień nie jest pusty, pętla uruchomi się ponownie po ostatnim prawidłowym wejściu. Ponieważ w ostatniej iteracji wszystkie >>operacje kończą się niepowodzeniem, zmienne prawdopodobnie zachowają swoją wartość z poprzedniej iteracji. Może się to objawiać jako „ostatni wiersz jest drukowany dwukrotnie” lub „ostatni rekord wejściowy jest przetwarzany dwa razy”.

    (Powinno to wyglądać nieco inaczej niż w C ++ 11 (patrz wyżej): Teraz otrzymujesz „widmowy rekord” zera zamiast powtarzanego ostatniego wiersza.)

  • Jeśli strumień zawiera zniekształcone dane, ale tylko je sprawdzasz .eof, powstaje nieskończona pętla. >>nie uda się wyodrębnić żadnych danych ze strumienia, więc pętla obraca się w miejscu, nie osiągając nigdy końca.


Reasumując: Rozwiązaniem jest przetestować sukces >>samej operacji, aby nie stosować oddzielną .eof()metodę: while (stream >> n >> m) { ... }podobnie jak w C przetestować sukces scanfsamego połączenia: while (scanf("%d%d", &n, &m) == 2) { ... }.

melpomene
źródło
1
jest to najbardziej dokładna odpowiedź, chociaż jak C ++ 11, Nie wierzę, że zmienne są już Niezainicjowane (pierwsza kula pt)
csguy