Qual è la vera ragione per non utilizzare il bit EOF come condizione di estrazione del stream?

Ispirato dalla mia precedente domanda

Un errore comune per i nuovi programmatori C ++ è quello di leggere da un file con qualcosa sulla falsariga di:

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

Riferiranno spesso che l’ultima riga del file è stata letta due volte. La spiegazione comune di questo problema (una che ho già fornito in precedenza) è simile a:

L’estrazione imposta il bit EOF sullo stream solo se si tenta di estrarre la fine del file, non se l’estrazione si interrompe solo alla fine del file. file.eof() ti dirà solo se la lettura precedente ha colpito la fine del file e non se la successiva sarà. Dopo che l’ultima riga è stata estratta, il bit EOF non è ancora impostato e l’iterazione si verifica ancora una volta. Tuttavia, in questa ultima iterazione, l’estrazione fallisce e la line ha ancora lo stesso contenuto di prima, cioè l’ultima riga è duplicata.

Tuttavia, la prima frase di questa spiegazione è sbagliata e quindi anche la spiegazione di ciò che sta facendo il codice è errata.

La definizione di funzioni di input formattate (quale operator>>(std::string&) è) definisce l’estrazione come usando rdbuf()->sbumpc() o rdbuf()->sgetc() per ottenere caratteri di input. Dichiara che se una di queste funzioni restituisce traits::eof() , allora il bit EOF è impostato:

Se rdbuf()->sbumpc() o rdbuf()->sgetc() restituisce traits::eof() , quindi la funzione di input, tranne come esplicitamente indicato diversamente, completa le sue azioni e setstate(eofbit) , che può generare ios_base::failure (27.5.5.4), prima di tornare.

Possiamo vedere questo con il semplice esempio che utilizza un file std::stringstream piuttosto che un file (sono entrambi flussi di input e si comportano allo stesso modo durante l’estrazione):

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

Qui è chiaro che l’estrazione singola ottiene hello dalla stringa e imposta il bit EOF su 1.

Allora, cosa c’è di sbagliato nella spiegazione? Cosa c’è di diverso sui file che causa !file.eof() per causare la duplicazione dell’ultima riga? Qual è il vero motivo per cui non dovremmo usare !file.eof() come nostra condizione di estrazione?

Sì, l’estrazione da un stream di input imposterà il bit EOF se l’estrazione si interrompe alla fine del file, come dimostrato dall’esempio std::stringstream . Se fosse così semplice, il ciclo con !file.eof() come sua condizione funzionerebbe perfettamente su un file come:

 hello world 

La seconda estrazione mangerebbe il world , fermandosi alla fine del file e di conseguenza impostando il bit EOF. La prossima iterazione non si verificherebbe.

Tuttavia, molti editor di testo hanno un segreto sporco. Stanno mentendo a te quando salvi un file di testo anche se così semplice. Quello che non ti dicono è che c’è un \n nascosto alla fine del file. Ogni riga del file termina con un \n , incluso l’ultimo. Quindi il file contiene effettivamente:

 hello\nworld\n 

Questo è ciò che causa la duplicazione dell’ultima riga quando si usa !file.eof() come condizione. Ora che lo sappiamo, possiamo vedere che la seconda estrazione mangerà il world fermandosi a \n e non impostando il bit EOF (perché non ci siamo ancora arrivati). Il ciclo verrà iterato per la terza volta ma la prossima estrazione fallirà perché non trova una stringa da estrarre, solo spazi bianchi. La stringa viene lasciata con il suo valore precedente ancora in giro e quindi otteniamo la linea duplicata.

Non lo fai con std::stringstream perché ciò che std::stringstream nello stream è esattamente ciò che ottieni. Non c’è \n alla fine di std::stringstream ss("hello") , a differenza del file. Se dovessi eseguire std::stringstream ss("hello\n") , potresti riscontrare lo stesso problema con la riga duplicata.

Quindi, ovviamente, possiamo vedere che non dovremmo mai usare !file.eof() come condizione per estrarre da un file di testo – ma qual è il vero problema qui? Perché dovremmo davvero non usarlo mai come nostra condizione, indipendentemente dal fatto che stiamo estraendo da un file o no?

Il vero problema è che eof() non ci dà alcuna idea se la prossima lettura fallirà o meno . Nel caso precedente, abbiamo visto che anche se eof() era 0, l’estrazione successiva non è riuscita perché non c’era alcuna stringa da estrarre. La stessa situazione accadrebbe se non associassimo un stream di file con nessun file o se il stream fosse vuoto. Il bit EOF non sarebbe impostato ma non c’è niente da leggere. Non possiamo andare avanti ciecamente ed estrarre dal file solo perché eof() non è impostato.

Usare while (std::getline(...)) e le condizioni correlate funziona perfettamente perché appena prima dell’inizio dell’estrazione, la funzione di input formattato verifica se sono stati impostati bit errati, non validi o EOF. Se uno di essi lo è, termina immediatamente, impostando il bit di fail nel processo. Fallirà anche se trova la fine del file prima di trovare ciò che vuole estrarre, impostando sia i bit di eof che di fail.


Nota: puoi salvare un file senza il \n aggiuntivo in vim se lo fai :set noeol e :set binary prima di salvare.

La tua domanda ha alcune concezioni fasulle. Tu dai una spiegazione:

“L’estrazione imposta il bit EOF sullo stream solo se si tenta di estrarre la fine del file, non se l’estrazione si interrompe solo alla fine del file.”

Quindi affermare che “è sbagliato e quindi anche la spiegazione di ciò che sta facendo il codice è sbagliata”.

In realtà, è giusto. Diamo un’occhiata a un esempio ….

Durante la lettura in una std::string

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

… per impostazione predefinita e come nella domanda l’ operator>> sta leggendo i caratteri finché non trova spazi bianchi o EOF. Così:

  • leggendo da 'abc\n' -> una volta incontrato '\n' non “tenta di estrarre la fine del file”, piuttosto “si ferma solo a [EOF]”, e eof() vinto ” t return true ,
  • leggendo da 'abc' invece -> è il tentativo di estrarre la fine del file che scopre la fine del contenuto della string , quindi eof() restituirà true .

Allo stesso modo, l’analisi di '123' in un insieme di eof() perché l’analisi non sa se ci sarà un’altra cifra e prova a continuare a leggerli, premendo eof() . L’analisi di '123 ' su un int non imposterà eof() .

Fondamentalmente, l’analisi di ‘a’ in un char non imposterà eof() perché lo spazio vuoto finale non è necessario per sapere che l’analisi è completa – una volta letto un carattere non viene fatto nessun tentativo per trovare un altro carattere e l’ eof() non è incontrati (Ovviamente un’ulteriore analisi dallo stesso stream colpisce eof ).

È chiaro [per stringstream “ciao” >> std :: string] che la singola estrazione ottiene hello dalla stringa e imposta il bit EOF su 1. Cosa c’è di sbagliato nella spiegazione? Cosa c’è di diverso sui file che causa! File.eof () per causare la duplicazione dell’ultima riga? Qual è il vero motivo per cui non dovremmo usare! File.eof () come nostra condizione di estrazione?

La ragione è come sopra … che i file tendono a essere terminati da un carattere ‘\ n’, e quando sono mezzi getline o >> std::string restituiscono l’ultimo token non-spazio vuoto senza aver bisogno di “tentare di estrarre il end-of-file “(per usare la tua frase).