Perché iostream :: eof all'interno di una condizione di loop (cioè `while (! Stream.eof ())`) è considerato errato?


595

Ho appena trovato un commento in questa risposta dicendo che l'uso iostream::eofin una condizione di loop è "quasi certamente sbagliato". In genere uso qualcosa del genere while(cin>>n)- che immagino controlli implicitamente per EOF.

Perché il controllo di eof utilizza esplicitamente un while (!cin.eof())errore?

In che cosa differisce dall'uso scanf("...",...)!=EOFin C (che uso spesso senza problemi)?


21
scanf(...) != EOFnon funzionerà neanche in C, perché scanfrestituisce il numero di campi analizzati e assegnati correttamente. La condizione corretta è scanf(...) < ndove si ntrova il numero di campi nella stringa di formato.
Ben Voigt,

5
@Ben Voigt, restituirà un numero negativo (che EOF di solito viene definito come tale) nel caso in cui venga raggiunto EOF
Sebastian,

19
@SebastianGodelet: In realtà, tornerà EOFse viene rilevata la fine del file prima della prima conversione del campo (riuscita o meno). Se viene raggiunta la fine del file tra i campi, verrà restituito il numero di campi convertiti e memorizzati con successo. Il che rende il confronto EOFsbagliato.
Ben Voigt,

1
@SebastianGodelet: No, non proprio. Si sbaglia quando dice che "oltre il ciclo non esiste un modo (facile) per distinguere un input corretto da uno improprio". In effetti è facile come controllare .eof()dopo l'uscita dal loop.
Ben Voigt,

2
@Ben Sì, per questo caso (leggendo un semplice int). Ma si può facilmente creare uno scenario in cui il while(fail)ciclo termina sia con un fallimento reale che con un eof. Pensa se hai bisogno di 3 ints per iterazione (supponi di leggere un punto xyz o qualcosa del genere), ma ci sono, erroneamente, solo due in nello stream.
furbo

Risposte:


544

Perché iostream::eoftornerà solo true dopo aver letto la fine dello stream. Esso non indica, che il prossimo lettura sarà la fine del flusso.

Considera questo (e presumi che la prossima lettura sarà alla fine dello stream):

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
}

Contro questo:

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

E sulla tua seconda domanda: Perché

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

equivale a

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

e non è lo stesso di

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

12
Vale la pena ricordare che if (! (InStream >> data) .eof ()) non fa nulla di utile. Fallacia 1: non entrerà nella condizione se non ci fosse spazio bianco dopo l'ultimo pezzo di dati (l'ultimo dato non verrà elaborato). Fallacia 2: entrerà nella condizione anche se la lettura dei dati non è riuscita, a condizione che EOF non sia stato raggiunto (ciclo infinito, che elabora ripetutamente gli stessi vecchi dati).
Tronic,

4
Penso che valga la pena sottolineare che questa risposta è leggermente fuorviante. Quando si estraggono intS o std::stringS o simili, il bit EOF viene impostato quando si estrae quello subito prima della fine e l'estrazione colpisce la fine. Non è necessario rileggere. Il motivo per cui non viene impostato durante la lettura dai file è perché \nalla fine c'è un extra . Ho trattato questo in un'altra risposta . Leggere chars è una questione diversa perché ne estrae solo una alla volta e non continua a colpire.
Joseph Mansfield,

79
Il problema principale è che solo perché non abbiamo raggiunto l'EOF, non significa che la lettura successiva avrà successo .
Joseph Mansfield,

1
@sftrabbit: tutto vero ma non molto utile ... anche se non c'è nessun trailing '\ n' è ragionevole volere che altri spazi bianchi finali vengano gestiti coerentemente con altri spazi bianchi nel file (cioè saltati). Inoltre, una sottile conseguenza di "quando si estrae quello giusto prima" è che while (!eof())non "funzionerà" su intse std::stringquando l'input è totalmente vuoto, quindi anche sapere che non \nè necessaria alcuna cura finale .
Tony Delroy,

2
@TonyD Totalmente d'accordo. Il motivo per cui lo dico è perché penso che la maggior parte delle persone quando leggono questo e risposte simili penseranno che se lo stream contiene "Hello"(senza spazi bianchi finali o \n) e std::stringviene estratto, estrarrà le lettere da Ha o, smetterà di estrarre e quindi non impostare il bit EOF. In effetti, imposterebbe il bit EOF perché fu l'EOF a fermare l'estrazione. Spero solo di chiarirlo per le persone.
Joseph Mansfield,

103

In conclusione: con una corretta gestione degli spazi bianchi, è eofpossibile utilizzare quanto segue (e persino, essere più affidabile rispetto fail()al controllo degli errori):

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

( Grazie a Tony D per il suggerimento di evidenziare la risposta. Vedi il suo commento qui sotto per un esempio del perché questo è più robusto. )


L'argomento principale contro l'uso eof()sembra mancare di una sottigliezza importante sul ruolo dello spazio bianco. La mia proposta è che, il controllo eof()esplicito non solo non sia " sempre sbagliato " - che sembra essere un'opinione prevalente in questo e simili thread SO -, ma con una corretta gestione degli spazi bianchi, fornisce un sistema più pulito e affidabile gestione degli errori, ed è la soluzione sempre corretta (anche se non necessariamente la più testata).

Riassumendo ciò che viene suggerito come terminazione "corretta" e ordine di lettura è il seguente:

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

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

L'errore dovuto al tentativo di lettura oltre eof viene considerato come condizione di terminazione. Ciò significa che non esiste un modo semplice per distinguere tra un flusso di successo e uno che non riesce realmente per ragioni diverse da eof. Prendi i seguenti flussi:

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

while(in>>data)termina con un set failbitper tutti e tre gli input. Nel primo e terzo, eofbitè anche impostato. Quindi, oltre il loop, è necessaria una logica extra molto brutta per distinguere un input corretto (1 °) da quelli impropri (2 ° e 3 °).

Considerando che, prendere quanto segue:

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

Qui, in.fail()verifica che fino a quando c'è qualcosa da leggere, è quello corretto. Il suo scopo non è un semplice terminatore di ciclo continuo.

Fin qui tutto bene, ma cosa succede se c'è spazio finale nel flusso - quale potrebbe sembrare la principale preoccupazione contro eof()come terminatore?

Non abbiamo bisogno di rinunciare alla nostra gestione degli errori; mangia lo spazio bianco:

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::wssalta qualsiasi potenziale (zero o più) spazio finale nello stream durante l'impostazione di eofbit, e non difailbit . Quindi, in.fail()funziona come previsto, purché ci sia almeno un dato da leggere. Se anche i flussi in bianco sono accettabili, la forma corretta è:

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
}

Riepilogo: una costruzione corretta while(!eof)non è solo possibile e non sbagliata, ma consente di localizzare i dati nell'ambito e fornisce una separazione più chiara del controllo degli errori dall'azienda come al solito. Detto questo, while(!fail)è indiscutibilmente un linguaggio più comune e conciso, e può essere preferito in scenari semplici (dati singoli per tipo di lettura).


6
" Quindi oltre il ciclo non esiste un modo (facile) per distinguere un input corretto da uno improprio. " Tranne che in un caso entrambi eofbite failbitsono impostati, nell'altro failbitè impostato. Devi solo verificarlo una volta dopo che il ciclo è terminato, non su ogni iterazione; lascerà il ciclo solo una volta, quindi devi solo controllare perché ha lasciato il ciclo una volta. while (in >> data)funziona bene per tutti i flussi vuoti.
Jonathan Wakely,

3
Quello che stai dicendo (e un punto sottolineato in precedenza) è che un flusso formattato male può essere identificato come !eof & failloop passato. Ci sono casi in cui non si può fare affidamento su questo. Vedi il commento sopra ( goo.gl/9mXYX ). Ad ogni modo, non sto proponendo eof-check come l' alternativa -sempre-migliore . Sto solo dicendo che è un modo possibile (e in alcuni casi più appropriato) di farlo, piuttosto che "sicuramente sbagliato!" come tende ad essere rivendicato qui in SO.
furbo

2
"Ad esempio, considera come verifichi gli errori in cui i dati sono una struttura con operatore sovraccarico >> lettura simultanea di più campi" - un caso molto più semplice che supporta il tuo punto è stream >> my_intdove lo stream contiene ad esempio "-": eofbite failbitsono impostato. È peggio dello operator>>scenario, in cui il sovraccarico fornito dall'utente ha almeno la possibilità di cancellare eofbitprima di tornare per aiutare a supportare l' while (s >> x)utilizzo. Più in generale, questa risposta potrebbe utilizzare una pulizia: solo il finale while( !(in>>ws).eof() )è generalmente robusto ed è sepolto alla fine.
Tony Delroy,

74

Perché se i programmatori non scrivono while(stream >> n), probabilmente scrivono questo:

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

Qui il problema è che non puoi fare a some work on nmeno di verificare prima se la lettura del flusso ha avuto successo, perché se non avesse successo, some work on nproduresti risultati indesiderati.

Il punto è che, eofbit, badbit, o failbitsono impostati dopo un tentativo di leggere dal flusso. Quindi se stream >> nfallisce, allora eofbit, badbito failbitviene impostato immediatamente, quindi è più idiomatico se scrivi while (stream >> n), perché l'oggetto restituito si streamconverte in falsecaso di errori nella lettura dal flusso e di conseguenza il ciclo si interrompe. E si converte in truese la lettura ha avuto successo e il ciclo continua.


1
A parte il menzionato "risultato indesiderato" con il lavoro sul valore indefinito di n, il programma potrebbe anche cadere in un ciclo infinito , se l'operazione di flusso non riuscita non consuma alcun input.
mastov,

10

Le altre risposte hanno spiegato perché la logica non è corretta while (!stream.eof())e come risolverla. Voglio concentrarmi su qualcosa di diverso:

perché il controllo di eof utilizza esplicitamente un iostream::eoferrore?

In termini generali, il controllo eof solo per è errato perché l'estrazione del flusso ( >>) può fallire senza colpire la fine del file. Se hai eg int n; cin >> n;e lo stream contiene hello, allora hnon è una cifra valida, quindi l'estrazione fallirà senza raggiungere la fine dell'input.

Questo problema, combinato con l'errore logico generale di controllo dello stato del flusso prima di tentare di leggerlo, il che significa che per N elementi di input il ciclo verrà eseguito N + 1 volte, porta ai seguenti sintomi:

  • Se il flusso è vuoto, il ciclo verrà eseguito una volta. >>fallirà (non c'è input da leggere) e tutte le variabili che avrebbero dovuto essere impostate (da stream >> x) sono in realtà non inizializzate. Questo porta all'elaborazione di dati spazzatura, che possono manifestarsi come risultati insensati (spesso numeri enormi).

    (Se la tua libreria standard è conforme a C ++ 11, ora le cose sono un po 'diverse: un errore >>ora imposta le variabili numeriche 0invece di lasciarle non inizializzate (tranne che per chars).)

  • Se il flusso non è vuoto, il ciclo verrà eseguito nuovamente dopo l'ultimo input valido. Poiché nell'ultima iterazione tutte le >>operazioni falliscono, è probabile che le variabili mantengano il loro valore dall'iterazione precedente. Questo può manifestarsi come "l'ultima riga viene stampata due volte" o "l'ultimo record di input viene elaborato due volte".

    (Ciò dovrebbe manifestarsi in modo leggermente diverso da C ++ 11 (vedi sopra): ora ottieni un "record fantasma" di zero anziché un'ultima riga ripetuta.)

  • Se il flusso contiene dati non validi ma si controlla solo per .eof, si finisce con un ciclo infinito. >>non riuscirà a estrarre alcun dato dallo stream, quindi il loop gira in posizione senza mai raggiungere la fine.


Per ricapitolare: La soluzione è quello di testare il successo della >>operazione stessa, di non usare un separato .eof()procedimento: while (stream >> n >> m) { ... }, proprio come in C si prova il successo della scanfchiamata stessa: while (scanf("%d%d", &n, &m) == 2) { ... }.


1
questa è la risposta più accurata, anche se a partire da c ++ 11, non credo che le variabili non siano più inizializzate (il primo punto pt)
csguy
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.