Warum wird iostream :: eof innerhalb einer Schleifenbedingung (dh `while (! Stream.eof ())`) als falsch angesehen?

595

Ich habe gerade einen Kommentar in dieser Antwort gefunden, der besagt, dass die Verwendung iostream::eofin einer Schleifenbedingung "mit ziemlicher Sicherheit falsch" ist. Ich benutze im Allgemeinen so etwas wie while(cin>>n)- was ich implizit auf EOF prüfe.

Warum wird die Überprüfung auf eof explizit while (!cin.eof())falsch verwendet?

Wie unterscheidet es sich von der Verwendung scanf("...",...)!=EOFin C (die ich oft ohne Probleme verwende)?

MAK
quelle
21
scanf(...) != EOFfunktioniert auch in C nicht, da scanfdie Anzahl der erfolgreich analysierten und zugewiesenen Felder zurückgegeben wird. Die richtige Bedingung ist, scanf(...) < nwo ndie Anzahl der Felder in der Formatzeichenfolge ist.
Ben Voigt
5
@ Ben Voigt, es wird eine negative Zahl (die EOF normalerweise als solche definiert wird) zurückgeben, falls EOF erreicht wird
Sebastian
19
@SebastianGodelet: Tatsächlich wird zurückgegeben, EOFwenn das Dateiende vor der ersten Feldkonvertierung festgestellt wurde (erfolgreich oder nicht). Wenn das Dateiende zwischen den Feldern erreicht ist, wird die Anzahl der Felder zurückgegeben, die erfolgreich konvertiert und gespeichert wurden. Was den Vergleich EOFfalsch macht.
Ben Voigt
1
@SebastianGodelet: Nein, nicht wirklich. Er irrt sich, wenn er sagt, dass "es nach der Schleife keine (einfache) Möglichkeit gibt, eine richtige Eingabe von einer falschen zu unterscheiden". Tatsächlich ist es so einfach wie das Überprüfen .eof()nach dem Beenden der Schleife.
Ben Voigt
2
@ Ben Ja, für diesen Fall (Lesen eines einfachen Int). Man kann sich jedoch leicht ein Szenario while(fail)ausdenken, in dem die Schleife sowohl mit einem tatsächlichen Fehler als auch mit einem Eof endet. Überlegen Sie, ob Sie 3 Zoll pro Iteration benötigen (sagen wir, Sie lesen einen xyz-Punkt oder etwas anderes), aber fälschlicherweise sind nur zwei Zoll im Stream.
Schlauer

Antworten:

544

Denn iostream::eofwird erst true nach dem Lesen des Stream-Endes zurückkehren. Es zeigt nicht an, dass der nächste Lesevorgang das Ende des Streams sein wird.

Bedenken Sie dies (und nehmen Sie an, dass der nächste Lesevorgang am Ende des Streams erfolgt):

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
}

Gegen das:

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

Und zu Ihrer zweiten Frage: Weil

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

ist das gleiche wie

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

und nicht das gleiche wie

if(!inStream.eof())
    inFile >> data
Xeo
quelle
12
Erwähnenswert ist, dass if (! (InStream >> data) .eof ()) auch nichts Nützliches tut. Irrtum 1: Die Bedingung wird nicht eingegeben, wenn nach dem letzten Datenelement kein Leerzeichen vorhanden war (das letzte Datum wird nicht verarbeitet). Irrtum 2: Die Bedingung wird auch dann eingegeben, wenn das Lesen von Daten fehlgeschlagen ist, solange EOF nicht erreicht wurde (Endlosschleife, wobei dieselben alten Daten immer wieder verarbeitet werden).
Tronic
4
Ich denke, es ist erwähnenswert, dass diese Antwort etwas irreführend ist. Beim Extrahieren von ints oder std::strings oder ähnlichem, der EOF - Bit wird gesetzt , wenn Sie die ein kurz vor dem Ende zu extrahieren und die Extraktion trifft das Ende. Sie müssen nicht erneut lesen. Der Grund, warum es beim Lesen aus Dateien nicht festgelegt wird, ist, dass es \nam Ende ein Extra gibt . Ich habe dies in einer anderen Antwort behandelt . Das Lesen von chars ist eine andere Sache, da es jeweils nur einen extrahiert und nicht weiter das Ende erreicht.
Joseph Mansfield
79
Das Hauptproblem ist, dass nur weil wir den EOF nicht erreicht haben, der nächste Lesevorgang nicht erfolgreich sein wird .
Joseph Mansfield
1
@sftrabbit: alles wahr, aber nicht sehr nützlich ... selbst wenn es kein abschließendes '\ n' gibt, ist es vernünftig zu wollen, dass andere abschließende Leerzeichen in der gesamten Datei konsistent mit anderen Leerzeichen behandelt werden (dh übersprungen). Eine subtile Konsequenz von "Wenn Sie die direkt zuvor extrahieren" ist, dass while (!eof())sie bei ints oder std::strings nicht "funktioniert", wenn die Eingabe vollständig leer ist. Selbst wenn Sie wissen, dass keine nachlaufende \nSorgfalt erforderlich ist.
Tony Delroy
2
@ TonyD stimme voll und ganz zu. Der Grund, warum ich das sage, ist, dass ich denke, dass die meisten Leute, wenn sie dies und ähnliche Antworten lesen, denken, wenn der Stream "Hello"(kein nachfolgendes Leerzeichen oder \n) enthält und a std::stringextrahiert wird, werden die Buchstaben von Hbis extrahiert, das Extrahieren owird gestoppt und dann nicht das EOF-Bit setzen. Tatsächlich würde es das EOF-Bit setzen, weil es die EOF war, die die Extraktion stoppte. Ich hoffe nur, das für die Leute zu klären.
Joseph Mansfield
103

Fazit oben: Bei richtiger Behandlung von Leerzeichen kann Folgendes eofverwendet werden (und sogar zuverlässiger sein als fail()bei der Fehlerprüfung):

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

( Vielen Dank an Tony D für den Vorschlag, die Antwort hervorzuheben. In seinem Kommentar unten finden Sie ein Beispiel dafür, warum dies robuster ist. )


Dem Hauptargument gegen die Verwendung eof()scheint eine wichtige Subtilität über die Rolle des Leerraums zu fehlen. Mein Vorschlag ist, dass eof()explizites Prüfen nicht nur nicht " immer falsch " ist - was in diesem und ähnlichen SO-Threads eine übergeordnete Meinung zu sein scheint -, sondern bei ordnungsgemäßem Umgang mit Leerraum auch für einen saubereren und zuverlässigeren Umgang sorgt Fehlerbehandlung und ist die immer richtige Lösung (obwohl nicht unbedingt die engste).

Um zusammenzufassen, was als "richtige" Kündigungs- und Lesereihenfolge vorgeschlagen wird, ist Folgendes:

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

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

Der Fehler aufgrund eines Leseversuchs über eof hinaus wird als Beendigungsbedingung angesehen. Dies bedeutet, dass es keine einfache Möglichkeit gibt, zwischen einem erfolgreichen Stream und einem Stream zu unterscheiden, der aus anderen Gründen als eof wirklich fehlschlägt. Nehmen Sie die folgenden Streams:

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

while(in>>data)endet mit einem Satz failbitfür alle drei Eingänge. Im ersten und dritten eofbitwird auch gesetzt. Nach der Schleife braucht man also eine sehr hässliche zusätzliche Logik, um einen richtigen Eingang (1.) von einem falschen (2. und 3.) zu unterscheiden.

Nehmen Sie Folgendes:

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

Hier wird in.fail()überprüft, ob etwas zu lesen ist, es ist das richtige. Sein Zweck ist nicht nur ein While-Loop-Terminator.

So weit so gut, aber was passiert, wenn im Stream nachlaufender Speicherplatz vorhanden ist - was klingt nach der größten Sorge eof()als Terminator?

Wir müssen unsere Fehlerbehandlung nicht aufgeben. iss einfach den Leerraum auf:

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Überspringt jeglichen potenziellen (null oder mehr) nachgestellten Speicherplatz im Stream, während das eofbitund nicht das festgelegt wirdfailbit . Funktioniert also in.fail()wie erwartet, solange mindestens eine Daten gelesen werden muss. Wenn auch leere Streams akzeptabel sind, lautet die richtige Form:

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
}

Zusammenfassung: Eine ordnungsgemäß erstellte Konstruktion while(!eof)ist nicht nur möglich und nicht falsch, sondern ermöglicht auch die Lokalisierung von Daten innerhalb des Gültigkeitsbereichs und bietet eine sauberere Trennung von Fehlerprüfung und Business as usual. Davon abgesehen, while(!fail)ist inarguably ein häufige und terse Idiom und kann in einfachen (Einzeldaten pro Lesetyp) Szenarien bevorzugt sein.

schlau
quelle
6
" Nach der Schleife gibt es also keine (einfache) Möglichkeit, eine richtige Eingabe von einer falschen zu unterscheiden. " Außer, dass in einem Fall beide eofbitund gesetzt failbitsind, in dem anderen nur failbitgesetzt. Sie müssen dies nur einmal testen, nachdem die Schleife beendet wurde, nicht bei jeder Iteration. Die Schleife wird nur einmal verlassen, sodass Sie nur einmal überprüfen müssen, warum sie die Schleife verlassen hat. while (in >> data)funktioniert gut für alle leeren Streams.
Jonathan Wakely
3
Was Sie sagen (und ein Punkt, der zuvor gemacht wurde), ist, dass ein schlecht formatierter Stream als !eof & failvergangene Schleife identifiziert werden kann . Es gibt Fälle, in denen man sich nicht darauf verlassen kann. Siehe obigen Kommentar ( goo.gl/9mXYX ). Auf jeden Fall schlage ich eof-check nicht als die immer bessere Alternative vor. Ich sage nur, es ist eine mögliche und (in einigen Fällen angemessenere) Art und Weise, dies zu tun, anstatt "mit Sicherheit falsch!" wie es hier in SO tendenziell behauptet wird.
Schlauer
2
„Als Beispiel betrachten wir, wie Sie auf Fehler überprüfen würde , wo die Daten eine Struktur mit überladenen Operator ist >> Lesen mehrere Felder auf einmal“ - ein viel einfacherer Fall , dass Ihr Punkt unterstützt wird , stream >> my_intwo der Strom enthält zB „-“: eofbitund failbitsind einstellen. Dies ist schlimmer als in dem operator>>Szenario, in dem die vom Benutzer bereitgestellte Überlastung zumindest die Option hat, eofbitvor der Rückkehr zu löschen, um die while (s >> x)Nutzung zu unterstützen . Im Allgemeinen könnte diese Antwort eine Bereinigung gebrauchen - nur das Finale while( !(in>>ws).eof() )ist im Allgemeinen robust und am Ende begraben.
Tony Delroy
74

Denn wenn Programmierer nicht schreiben while(stream >> n), schreiben sie möglicherweise Folgendes:

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

Hier besteht das Problem darin, dass Sie nicht darauf verzichten müssen, some work on nzuerst zu überprüfen, ob das Lesen des Streams erfolgreich war. Wenn dies nicht erfolgreich war, some work on nwürde dies zu unerwünschten Ergebnissen führen.

Der springende Punkt ist , dass, eofbit, badbit, oder failbiteingestellt werden , nachdem ein Versuch gemacht wird , aus dem Stream zu lesen. Also , wenn stream >> nnicht, dann eofbit, badbitoder failbitsofort gesetzt wird, so dass ihre mehr idiomatischen , wenn Sie schreiben while (stream >> n), weil das zurückgegebene Objekt streamKonvertiten , falsewenn irgendein Fehler war aus dem Strom in dem Lese und damit die Schleife beendet. Und es wird konvertiert, truewenn der Lesevorgang erfolgreich war und die Schleife fortgesetzt wird.

Nawaz
quelle
1
Abgesehen von dem erwähnten "unerwünschten Ergebnis" bei der Arbeit am undefinierten Wert von nkann das Programm auch in eine Endlosschleife fallen , wenn die fehlgeschlagene Stream-Operation keine Eingabe verbraucht.
Mastov
10

Die anderen Antworten haben erklärt, warum die Logik falsch ist while (!stream.eof())und wie man sie behebt. Ich möchte mich auf etwas anderes konzentrieren:

Warum wird die Überprüfung auf eof explizit iostream::eoffalsch verwendet?

Im Allgemeinen ist die Überprüfung eof nur auf falsch, da die Stream-Extraktion ( >>) fehlschlagen kann, ohne das Ende der Datei zu erreichen. Wenn Sie zB haben int n; cin >> n;und der Stream enthält hello, hist dies keine gültige Ziffer, sodass die Extraktion fehlschlägt, ohne das Ende der Eingabe zu erreichen.

Dieses Problem führt zusammen mit dem allgemeinen logischen Fehler, den Stream-Status vor dem Versuch, daraus zu lesen, zu überprüfen , was bedeutet, dass die Schleife für N Eingabeelemente N + 1 Mal ausgeführt wird, zu den folgenden Symptomen:

  • Wenn der Stream leer ist, wird die Schleife einmal ausgeführt. >>wird fehlschlagen (es gibt keine zu lesende Eingabe) und alle Variablen, die gesetzt werden sollten (von stream >> x), sind tatsächlich nicht initialisiert. Dies führt dazu, dass Mülldaten verarbeitet werden, die sich als unsinnige Ergebnisse (häufig große Zahlen) manifestieren können.

    (Wenn Ihre Standardbibliothek C ++ 11 entspricht, sind die Dinge jetzt etwas anders: Ein Fehler >>setzt jetzt numerische Variablen auf, 0anstatt sie nicht initialisiert zu lassen (mit Ausnahme von chars).)

  • Wenn der Stream nicht leer ist, wird die Schleife nach der letzten gültigen Eingabe erneut ausgeführt. Da in der letzten Iteration alle >>Operationen fehlschlagen, behalten Variablen wahrscheinlich ihren Wert aus der vorherigen Iteration bei. Dies kann sich als "die letzte Zeile wird zweimal gedruckt" oder "der letzte Eingabedatensatz wird zweimal verarbeitet" manifestieren.

    (Dies sollte sich seit C ++ 11 etwas anders manifestieren (siehe oben): Jetzt erhalten Sie einen "Phantom Record" von Nullen anstelle einer wiederholten letzten Zeile.)

  • Wenn der Stream fehlerhafte Daten enthält, Sie aber nur nachsehen .eof, erhalten Sie eine Endlosschleife. >>Es werden keine Daten aus dem Stream extrahiert, sodass sich die Schleife dreht, ohne jemals das Ende zu erreichen.


Um es noch einmal zusammenzufassen: Die Lösung besteht darin, den Erfolg der >>Operation selbst zu testen und keine separate .eof()Methode zu verwenden: while (stream >> n >> m) { ... }Genau wie in C testen Sie den Erfolg des scanfAufrufs selbst : while (scanf("%d%d", &n, &m) == 2) { ... }.

Melpomene
quelle
1
Dies ist die genaueste Antwort, obwohl ich ab c ++ 11 nicht mehr glaube, dass die Variablen nicht mehr initialisiert sind (der erste
Punkt