¿Por qué iostream :: eof dentro de una condición de bucle (es decir, `while (! Stream.eof ())`) se considera incorrecto?

595

Acabo de encontrar un comentario en esta respuesta que dice que usar iostream::eofen una condición de bucle es "casi seguro que está mal". Generalmente uso algo como while(cin>>n), que supongo que comprueba implícitamente EOF.

¿Por qué es while (!cin.eof())incorrecto verificar eof explícitamente ?

¿Cómo es diferente de usar scanf("...",...)!=EOFen C (que a menudo uso sin problemas)?

MAK
fuente
21
scanf(...) != EOFtampoco funcionará en C, porque scanfdevuelve el número de campos analizados y asignados correctamente. La condición correcta es scanf(...) < ndónde nestá el número de campos en la cadena de formato.
Ben Voigt
55
@Ben Voigt, devolverá un número negativo (que EOF generalmente se define como tal) en caso de que se alcance EOF
Sebastian
19
@SebastianGodelet: en realidad, volverá EOFsi se encuentra el final del archivo antes de la primera conversión de campo (exitosa o no). Si se alcanza el final del archivo entre los campos, devolverá el número de campos convertidos y almacenados con éxito. Lo que hace la comparación con el EOFmal.
Ben Voigt
1
@SebastianGodelet: No, en realidad no. Se equivoca cuando dice que "más allá del bucle no hay forma (fácil) de distinguir una entrada adecuada de una incorrecta". De hecho, es tan fácil como verificar .eof()después de que sale el bucle.
Ben Voigt
2
@Ben Sí, para este caso (leyendo un int simple). Pero se puede llegar fácilmente a un escenario en el que el while(fail)ciclo termina con un fallo real y un eof. Piense si necesita 3 ints por iteración (digamos que está leyendo un punto xyz o algo así), pero, erróneamente, solo hay dos ints en la secuencia.
astuto

Respuestas:

544

Porque iostream::eofsolo regresará true después de leer el final de la transmisión. No , no indican, que la siguiente lectura será el final de la secuencia.

Considere esto (y suponga que la próxima lectura será al final de la secuencia):

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
}

Contra esto:

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

Y en su segunda pregunta: porque

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

es lo mismo que

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

y no es lo mismo que

if(!inStream.eof())
    inFile >> data
Xeo
fuente
12
Vale la pena mencionar que si (! (InStream >> data) .eof ()) tampoco hace nada útil. Falacia 1: no entrará en la condición si no hubo espacios en blanco después del último dato (el último dato no se procesará). Fallacy 2: entrará en la condición incluso si falla la lectura de datos, siempre que no se haya alcanzado EOF (bucle infinito, procesando los mismos datos antiguos una y otra vez).
Tronic
44
Creo que vale la pena señalar que esta respuesta es un poco engañosa. Al extraer intso std::strings o similar, el bit EOF se establece cuando extrae el justo antes del final y la extracción llega al final. No necesita leer de nuevo. La razón por la que no se configura al leer archivos es porque hay un extra \nal final. He cubierto esto en otra respuesta . Leer chars es un asunto diferente porque solo extrae uno a la vez y no continúa hasta el final.
Joseph Mansfield
79
El principal problema es que el hecho de que no hayamos alcanzado el EOF no significa que la próxima lectura tendrá éxito .
Joseph Mansfield
1
@sftrabbit: todo es cierto pero no muy útil ... incluso si no hay '\ n' finales, es razonable querer que otros espacios en blanco finales se manejen de manera coherente con otros espacios en blanco en todo el archivo (es decir, omitidos). Además, una consecuencia sutil de "cuando extrae el justo antes" es que while (!eof())no "funcionará" en ints o std::strings cuando la entrada está totalmente vacía, por lo que incluso sabiendo que no \nse necesita cuidado adicional.
Tony Delroy el
2
@TonyD Totalmente de acuerdo. La razón por la que lo digo es porque creo que la mayoría de las personas cuando leen esto y respuestas similares pensarán que si la secuencia contiene "Hello"(sin espacios en blanco o \n) y std::stringse extrae, extraerá las letras de Ha o, dejará de extraer y entonces no establezca el bit EOF. De hecho, establecería el bit EOF porque fue el EOF el que detuvo la extracción. Solo esperando aclarar eso para la gente.
Joseph Mansfield
103

Parte superior de la línea inferior: con el manejo adecuado del espacio en blanco, lo siguiente es cómo eofse puede usar (e incluso, ser más confiable que fail()para la verificación de errores):

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

( Gracias Tony D por la sugerencia de resaltar la respuesta. Vea su comentario a continuación para ver un ejemplo de por qué esto es más robusto ) .


Al argumento principal en contra del uso eof()parece faltarle una sutileza importante sobre el papel del espacio en blanco. Mi propuesta es que, verificar eof()explícitamente no solo no es " siempre incorrecto ", lo que parece ser una opinión primordial en este y otros hilos SO similares, sino que, con un manejo adecuado del espacio en blanco, proporciona un espacio más limpio y más confiable manejo de errores, y es la solución siempre correcta (aunque, no necesariamente la más estricta).

Para resumir lo que se sugiere como la terminación "correcta" y el orden de lectura es el siguiente:

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

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

La falla debida al intento de lectura más allá de eof se toma como la condición de terminación. Esto significa que no hay una manera fácil de distinguir entre una transmisión exitosa y una que realmente falla por otras razones que no sean eof. Tome las siguientes transmisiones:

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

while(in>>data)termina con un conjunto failbitpara las tres entradas. En el primero y tercero, eofbittambién se establece. Entonces, más allá del bucle, uno necesita una lógica extra muy fea para distinguir una entrada adecuada (primera) de las incorrectas (segunda y tercera).

Mientras que, tome lo siguiente:

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

Aquí, in.fail()verifica que mientras haya algo para leer, es el correcto. Su propósito no es un simple terminador while-loop.

Hasta ahora todo bien, pero ¿qué sucede si hay un espacio final en la secuencia? ¿Cuál parece ser la principal preocupación contra el eof()terminador?

No necesitamos entregar nuestro manejo de errores; solo come el espacio en blanco:

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::wsomite cualquier espacio final potencial (cero o más) en la secuencia al configurar eofbity no elfailbit . Por lo tanto, in.fail()funciona según lo esperado, siempre que haya al menos un dato para leer. Si las secuencias en blanco también son aceptables, entonces la forma correcta es:

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
}

Resumen: Una construcción adecuada while(!eof)no solo es posible y no está mal, sino que permite que los datos se localicen dentro del alcance y proporciona una separación más clara de la verificación de errores de la empresa como de costumbre. Dicho esto, while(!fail)es indiscutiblemente un lenguaje más común y conciso, y puede preferirse en escenarios simples (datos únicos por tipo de lectura).

astuto
fuente
66
" Así que más allá del bucle no hay manera (fácil) para distinguir una entrada adecuada a partir de uno inadecuado. " Excepto que en un caso tanto eofbity failbitse establecen, en el otro sólo failbitse establece. Solo necesita probar eso una vez que el ciclo ha terminado, no en cada iteración; solo dejará el bucle una vez, por lo que solo debe verificar por qué lo dejó una vez. while (in >> data)funciona bien para todas las transmisiones en blanco.
Jonathan Wakely
3
Lo que está diciendo (y un punto mencionado anteriormente) es que una secuencia con formato incorrecto puede identificarse como un !eof & failbucle pasado. Hay casos en los que uno no puede confiar en esto. Ver comentario anterior ( goo.gl/9mXYX ). De todos modos, no estoy proponiendo eof-check como la alternativa siempre mejor . Solo digo que es una forma posible y (en algunos casos más apropiada) de hacer esto, en lugar de "¡ciertamente equivocado!" como tiende a ser reclamado por aquí en SO.
astuto
2
"Como ejemplo, considere cómo verificaría si los datos son una estructura con un operador sobrecargado >> leer varios campos a la vez" - un caso mucho más simple que respalda su punto es stream >> my_intdonde la secuencia contiene, por ejemplo, "-": eofbity failbitson conjunto. Eso es peor que el operator>>escenario, donde la sobrecarga proporcionada por el usuario al menos tiene la opción de borrar eofbitantes de regresar para ayudar a while (s >> x)usar el soporte . En términos más generales, esta respuesta podría usar una limpieza: solo el final while( !(in>>ws).eof() )es generalmente robusto y está enterrado al final.
Tony Delroy
74

Porque si los programadores no escriben while(stream >> n), posiblemente escriban esto:

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

Aquí el problema es que no puede hacerlo some work on nsin verificar primero si la lectura de la transmisión fue exitosa, porque si no tuvo éxito, some work on nproduciría un resultado no deseado.

El punto es que, eofbit, badbit, o failbitse establece después de que se hizo un intento de leer de la corriente. Entonces, si stream >> nfalla, entonces eofbit, badbito failbitse establece de inmediato, por lo que es más idiomático si escribe while (stream >> n), porque el objeto devuelto se streamconvierte en falsesi hubo algún error en la lectura de la secuencia y, en consecuencia, el ciclo se detiene. Y se convierte en truesi la lectura fue exitosa y el ciclo continúa.

Nawaz
fuente
1
Además del mencionado "resultado no deseado" al trabajar en el valor indefinido de n, el programa también puede caer en un bucle infinito , si la operación de flujo fallido no consume ninguna entrada.
mastov
10

Las otras respuestas han explicado por qué la lógica es incorrecta while (!stream.eof())y cómo solucionarlo. Quiero centrarme en algo diferente:

¿Por qué es iostream::eofincorrecto verificar eof explícitamente ?

En términos generales, verificar eof solo es incorrecto porque la extracción de flujo ( >>) puede fallar sin llegar al final del archivo. Si tiene, por ejemplo, int n; cin >> n;y la secuencia contiene hello, entonces hno es un dígito válido, por lo que la extracción fallará sin llegar al final de la entrada.

Este problema, combinado con el error lógico general de verificar el estado del flujo antes de intentar leerlo, lo que significa que para N elementos de entrada el ciclo se ejecutará N + 1 veces, conduce a los siguientes síntomas:

  • Si la secuencia está vacía, el ciclo se ejecutará una vez. >>fallará (no hay entrada para leer) y todas las variables que se suponía que debían establecerse (por stream >> x) en realidad no se inicializan. Esto lleva a que se procesen datos basura, que pueden manifestarse como resultados sin sentido (a menudo, cantidades enormes).

    (Si su biblioteca estándar se ajusta a C ++ 11, las cosas son un poco diferentes ahora: un error >>ahora establece variables numéricas en 0lugar de dejarlas sin inicializar (excepto para chars)).

  • Si la secuencia no está vacía, el ciclo se ejecutará nuevamente después de la última entrada válida. Dado que en la última iteración todas las >>operaciones fallan, es probable que las variables conserven su valor de la iteración anterior. Esto puede manifestarse como "la última línea se imprime dos veces" o "el último registro de entrada se procesa dos veces".

    (Esto debería manifestarse un poco diferente desde C ++ 11 (ver arriba): ahora obtienes un "registro fantasma" de ceros en lugar de una última línea repetida).

  • Si la secuencia contiene datos mal formados pero solo los verifica .eof, termina con un bucle infinito. >>no podrá extraer ningún dato de la transmisión, por lo que el bucle gira en su lugar sin llegar al final.


En resumen: La solución es poner a prueba el éxito de la >>operación en sí, no usar una separada .eof()método: while (stream >> n >> m) { ... }al igual que en C se prueba el éxito de la scanfmisma llamada: while (scanf("%d%d", &n, &m) == 2) { ... }.

melpomene
fuente
1
esta es la respuesta más precisa, aunque a partir de c ++ 11, ya no creo que las variables no estén inicializadas (el primer punto de viñeta)
csguy