Por que o iostream :: eof dentro de uma condição de loop (ou seja, `while (! Stream.eof ())`) é considerado errado?

595

Acabei de encontrar um comentário nesta resposta dizendo que o uso iostream::eofem uma condição de loop é "quase certamente errado". Eu geralmente uso algo como while(cin>>n)- o que eu acho implicitamente verifica EOF.

Por que a verificação de eof está explicitamente usando while (!cin.eof())errado?

Como é diferente de usar scanf("...",...)!=EOFem C (que eu costumo usar sem problemas)?

MAK
fonte
21
scanf(...) != EOFtambém não funcionará em C, porque scanfretorna o número de campos analisados ​​e atribuídos com êxito. A condição correta é scanf(...) < nonde nestá o número de campos na string de formato.
quer
5
@ Ben Voigt, ele vai devolver um número negativo (EOF que geralmente é definido como tal) em caso EOF é atingido
Sebastian
19
@SebastianGodelet: Na verdade, ele retornará EOFse o final do arquivo for encontrado antes da primeira conversão do campo (bem-sucedida ou não). Se o final do arquivo for alcançado entre os campos, ele retornará o número de campos convertidos e armazenados com êxito. O que faz a comparação EOFerrada.
precisa
1
@SebastianGodelet: Não, na verdade não. Ele erra quando diz que "após o ciclo não existe uma maneira (fácil) de distinguir uma entrada adequada de uma entrada imprópria". Na verdade, é tão fácil quanto verificar .eof()depois que o loop termina.
precisa
2
@ Ben Sim, para este caso (lendo um simples int). Mas pode-se facilmente chegar a um cenário em que o while(fail)loop termina com uma falha real e um Eof. Pense se você precisar de 3 ints por iteração (digamos que esteja lendo um ponto xyz ou algo assim), mas há, erroneamente, apenas duas ints no fluxo.
sly

Respostas:

544

Porque iostream::eofsó retornará true depois de ler o final do fluxo. Isso não indica que a próxima leitura será o fim do fluxo.

Considere isso (e assuma que a próxima leitura será no final do fluxo):

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 isso:

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 na sua segunda pergunta: porque

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

é o mesmo que

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

e não é o mesmo que

if(!inStream.eof())
    inFile >> data
Xeo
fonte
12
Vale ressaltar que se (! (InStream >> data) .eof ()) também não faz nada útil. Falácia 1: não entrará na condição se não houvesse espaço em branco após a última parte de dados (o último dado não será processado). Falácia 2: Ele entrará na condição mesmo se a leitura dos dados falhar, desde que o EOF não seja atingido (loop infinito, processando os mesmos dados antigos repetidamente).
Tronic
4
Acho que vale ressaltar que essa resposta é um pouco enganadora. Ao extrair ints ou std::strings ou similar, o bit EOF é definido quando você extrai o mesmo antes do final e a extração atinge o final. Você não precisa ler novamente. O motivo de não ser definido ao ler arquivos é porque há um extra \nno final. Eu cobri isso em outra resposta . A leitura de chars é uma questão diferente porque extrai apenas uma de cada vez e não continua chegando ao fim.
31713 Joseph Mansfield
79
O principal problema é que o fato de não termos alcançado o EOF não significa que a próxima leitura será bem-sucedida .
31813 Joseph Mansfield
1
@sftrabbit: tudo verdadeiro, mas não muito útil ... mesmo que não haja '\ n' à direita, é razoável querer que outro espaço em branco à direita seja tratado consistentemente com outro espaço em branco no arquivo (ou seja, ignorado). Além disso, uma conseqüência sutil de "quando você extrai o item anterior" é que while (!eof())não "funcionará" em ints ou std::strings quando a entrada estiver totalmente vazia, portanto, mesmo sabendo que não há \ncuidados finais , é necessário.
Tony Delroy
2
@TonyD Concordo totalmente. A razão pela qual estou dizendo isso é porque acho que a maioria das pessoas quando lêem isso e respostas semelhantes pensam que, se o fluxo contiver "Hello"(sem espaços em branco à direita ou \n) e a std::stringfor extraído, ele extrairá as letras de Hpara o, interromperá a extração e então não defina o bit EOF. De fato, ele definiria o bit EOF porque foi o EOF que interrompeu a extração. Apenas esperando esclarecer isso para as pessoas.
Joseph Mansfield
103

Conclusão: com o manuseio adequado do espaço em branco, a seguir, é eofpossível usar (e até ser mais confiável do que fail()a verificação de erros):

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

( Agradecemos a Tony D pela sugestão de destacar a resposta. Veja o comentário abaixo para um exemplo de por que isso é mais robusto. )


O principal argumento contra o uso eof()parece estar faltando uma sutileza importante sobre o papel do espaço em branco. Minha proposição é que, verificar eof()explicitamente não apenas não está " sempre errado " - o que parece ser uma opinião predominante nesse e nos tópicos similares -, mas com o manuseio adequado do espaço em branco, ele fornece um ambiente mais limpo e confiável. tratamento de erros e é a solução sempre correta (embora não necessariamente a mais difícil).

Para resumir o que está sendo sugerido como a ordem de finalização e leitura "adequada", é o seguinte:

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

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

A falha devido à tentativa de leitura além de eof é tomada como condição de finalização. Isso significa que não há uma maneira fácil de distinguir entre um fluxo bem-sucedido e um que realmente falha por outros motivos que não o eof. Tome os seguintes fluxos:

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

while(in>>data)termina com um conjunto failbitpara todas as três entradas. No primeiro e terceiro, eofbittambém está definido. Portanto, após o loop, é necessário uma lógica extra muito feia para distinguir uma entrada adequada (1ª) das impróprias (2ª e 3ª).

Visto que, faça o seguinte:

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

Aqui, in.fail()verifica se, desde que haja algo para ler, é o correto. Seu objetivo não é um mero terminador de loop while.

Até aí tudo bem, mas o que acontece se houver espaço à direita no fluxo - o que parece ser a principal preocupação contra eof()o terminador?

Não precisamos renunciar ao tratamento de erros; apenas coma o espaço em branco:

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::wspula qualquer espaço potencial (zero ou mais) à direita no fluxo enquanto define o eofbit, e não ofailbit . Portanto, in.fail()funciona conforme o esperado, desde que haja pelo menos um dado para ler. Se todos os fluxos em branco também forem aceitáveis, o formato correto será:

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
}

Resumo: Uma construção adequada while(!eof)não é apenas possível e incorreta, mas permite que os dados sejam localizados no escopo e fornece uma separação mais limpa da verificação de erros dos negócios, como de costume. Dito isto, while(!fail)é indiscutivelmente um idioma mais comum e conciso, e pode ser preferido em cenários simples (dados únicos por tipo de leitura).

astuto
fonte
6
Portanto, além do loop, não existe uma maneira (fácil) de distinguir uma entrada adequada de uma entrada imprópria. ” Exceto que, em um caso, ambos eofbite failbitestão configurados, no outro apenas failbitestá configurado. Você só precisa de teste que uma vez após o loop tenha terminado, não em cada iteração; ele sairá do loop apenas uma vez; portanto, você só precisa verificar por que ele saiu do loop uma vez. while (in >> data)funciona bem para todos os fluxos em branco.
Jonathan Wakely
3
O que você está dizendo (e um argumento mencionado anteriormente) é que um fluxo formatado incorreto pode ser identificado como !eof & failloop passado. Há casos em que não se pode confiar nisso. Veja o comentário acima ( goo.gl/9mXYX ). De qualquer forma, não estou propondo eof-check como a sempre-melhor alternativa. Estou apenas dizendo que é uma maneira possível e (em alguns casos mais apropriada) de fazer isso, em vez de "certamente errado!" como tende a ser reivindicado por aqui no SO.
sly
2
"Como exemplo, considere como você verificaria erros onde os dados são uma estrutura com operador sobrecarregado >> lendo vários campos ao mesmo tempo" - um caso muito mais simples que suporta seu argumento é stream >> my_intonde o fluxo contém, por exemplo, "-": eofbite failbitestá conjunto. Isso é pior do que o operator>>cenário, onde a sobrecarga fornecida pelo usuário tem pelo menos a opção de limpar eofbitantes de retornar para ajudar no suporte ao while (s >> x)uso. De maneira mais geral, essa resposta pode ser usada como limpeza - apenas a final while( !(in>>ws).eof() )é geralmente robusta e está enterrada no final.
Tony Delroy 25/02
74

Porque se os programadores não escrevem while(stream >> n), eles possivelmente escrevem o seguinte:

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

Aqui está o problema: você não pode ficar some work on nsem verificar primeiro se a leitura do fluxo foi bem-sucedida, porque, se não tivesse êxito, o some work on nresultado seria indesejável.

A questão toda é que, eofbit, badbit, ou failbitsão definidas depois é feita uma tentativa de ler a partir do fluxo. Portanto, se stream >> nfalhar, então eofbit, badbitou failbitserá definido imediatamente, será mais idiomático se você escrever while (stream >> n), porque o objeto retornado será streamconvertido falsese houver alguma falha na leitura do fluxo e, consequentemente, o loop for interrompido. E converte truese a leitura foi bem-sucedida e o loop continua.

Nawaz
fonte
1
Além do "resultado indesejado" mencionado, com o trabalho no valor indefinido de n, o programa também pode cair em um loop infinito , se a operação de fluxo com falha não consumir nenhuma entrada.
Mašťov
10

As outras respostas explicaram por que a lógica está errada while (!stream.eof())e como corrigi-la. Eu quero focar em algo diferente:

por que a verificação de eof está explicitamente usando iostream::eoferrado?

Em termos gerais, a verificação de eof apenas está errada porque a extração do fluxo ( >>) pode falhar sem atingir o final do arquivo. Se você tem, por exemplo, int n; cin >> n;e o fluxo contém hello, então hnão é um dígito válido, portanto a extração falhará sem atingir o final da entrada.

Esse problema, combinado com o erro lógico geral de verificar o estado do fluxo antes de tentar ler dele, o que significa que para N itens de entrada o loop será executado N + 1 vezes, leva aos seguintes sintomas:

  • Se o fluxo estiver vazio, o loop será executado uma vez. >>falhará (não há entrada a ser lida) e todas as variáveis ​​que deveriam ser definidas (por stream >> x) são realmente não inicializadas. Isso leva ao processamento de dados de lixo, que podem se manifestar como resultados sem sentido (geralmente grandes números).

    (Se a sua biblioteca padrão estiver em conformidade com o C ++ 11, as coisas estão um pouco diferentes agora: agora, uma falha >>define as variáveis ​​numéricas em 0vez de deixá-las não inicializadas (exceto chars).)

  • Se o fluxo não estiver vazio, o loop será executado novamente após a última entrada válida. Como na última iteração todas as >>operações falham, é provável que as variáveis ​​mantenham seu valor da iteração anterior. Isso pode se manifestar como "a última linha é impressa duas vezes" ou "o último registro de entrada é processado duas vezes".

    (Isso deve se manifestar um pouco diferente desde C ++ 11 (veja acima): Agora você obtém um "registro fantasma" de zeros em vez de uma última linha repetida.)

  • Se o fluxo contiver dados malformados, mas você apenas verificar .eof, você terá um loop infinito. >>falhará ao extrair quaisquer dados do fluxo; portanto, o loop gira no lugar sem nunca chegar ao fim.


Para recapitular: A solução é testar o sucesso da >>operação em si, para não usar um separar .eof()método: while (stream >> n >> m) { ... }, tal como em C você testar o sucesso da scanfprópria chamada: while (scanf("%d%d", &n, &m) == 2) { ... }.

melpomene
fonte
1
esta é a resposta mais precisa, embora, como de c ++ 11, eu não acredito que as variáveis são mais (a primeira bala pt) não inicializado
csguy