Mengapa iostream :: eof di dalam kondisi loop (yaitu `while (! Stream.eof ())`) dianggap salah?

595

Saya baru saja menemukan komentar dalam jawaban ini yang mengatakan bahwa menggunakan iostream::eofdalam kondisi loop adalah "hampir pasti salah". Saya biasanya menggunakan sesuatu seperti while(cin>>n)- yang saya kira secara implisit memeriksa EOF.

Mengapa memeriksa bukti penggunaan while (!cin.eof())salah secara eksplisit ?

Apa bedanya dengan menggunakan scanf("...",...)!=EOFC (yang sering saya gunakan tanpa masalah)?

MAK
sumber
21
scanf(...) != EOFjuga tidak akan berfungsi di C, karena scanfmengembalikan jumlah bidang yang berhasil diuraikan dan ditugaskan. Kondisi yang benar adalah di scanf(...) < nmana njumlah bidang dalam string format.
Ben Voigt
5
@Ben Voigt, itu akan mengembalikan angka negatif (yang EOF biasanya didefinisikan seperti itu) jika EOF tercapai
Sebastian
19
@SebastianGodelet: Sebenarnya, itu akan kembali EOFjika akhir file ditemukan sebelum konversi bidang pertama (berhasil atau tidak). Jika akhir file dicapai di antara bidang, itu akan mengembalikan jumlah bidang yang berhasil dikonversi dan disimpan. Yang membuat perbandingan dengan yang EOFsalah.
Ben Voigt
1
@SebastianGodelet: Tidak, tidak juga. Dia keliru ketika dia mengatakan bahwa "melewati loop tidak ada (mudah) cara untuk membedakan input yang tepat dari yang tidak tepat". Sebenarnya itu semudah memeriksa .eof()setelah loop keluar.
Ben Voigt
2
@ Ben Ya, untuk kasus ini (membaca int sederhana). Tetapi orang dapat dengan mudah membuat skenario di mana while(fail)loop berakhir dengan kegagalan aktual dan bukti. Pikirkan jika Anda memerlukan 3 int per iterasi (katakanlah Anda membaca titik xyz atau sesuatu), tetapi ada, secara keliru, hanya dua int dalam stream.
licik

Jawaban:

544

Karena iostream::eofhanya akan kembali true setelah membaca akhir aliran. Itu tidak menunjukkan, bahwa bacaan berikutnya akan menjadi akhir dari aliran.

Pertimbangkan ini (dan anggap bacaan selanjutnya akan berada di akhir arus):

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
}

Terhadap ini:

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

Dan pada pertanyaan kedua Anda: Karena

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

sama dengan

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

dan tidak sama dengan

if(!inStream.eof())
    inFile >> data
Xeo
sumber
12
Layak disebutkan adalah bahwa jika (! (InStream >> data) .eof ()) tidak melakukan hal yang berguna juga. Kekeliruan 1: Ini tidak akan memasuki kondisi jika tidak ada spasi setelah data terakhir (datum terakhir tidak akan diproses). Kekeliruan 2: Ini akan memasuki kondisi bahkan jika pembacaan data gagal, selama EOF tidak tercapai (infinite loop, memproses data lama yang sama berulang-ulang).
Tronic
4
Saya pikir ada baiknya menunjukkan bahwa jawaban ini sedikit menyesatkan. Ketika penggalian ints atau std::strings atau serupa, sedikit EOF adalah mengatur kapan Anda ekstrak yang tepat sebelum akhir dan ekstraksi hits akhir. Anda tidak perlu membaca lagi. Alasan tidak diatur saat membaca dari file adalah karena ada tambahan \ndi akhir. Saya sudah membahas ini dalam jawaban lain . Membaca chars adalah masalah yang berbeda karena hanya mengekstrak satu per satu dan tidak terus mencapai akhir.
Joseph Mansfield
79
Masalah utama adalah bahwa hanya karena kita belum mencapai EOF, tidak berarti pembacaan berikutnya akan berhasil .
Joseph Mansfield
1
@ sftrabbit: semua benar tetapi tidak terlalu berguna ... bahkan jika tidak ada trailing '\ n' masuk akal untuk menginginkan spasi spasi tambahan lainnya ditangani secara konsisten dengan spasi putih lainnya di seluruh file (mis. dilewati). Lebih jauh, konsekuensi halus dari "ketika Anda mengekstrak yang benar sebelum" adalah bahwa while (!eof())itu tidak akan "bekerja" pada intatau std::stringketika input benar-benar kosong, sehingga bahkan mengetahui tidak ada \nperawatan trailing diperlukan.
Tony Delroy
2
@ TonyD Sangat setuju. Alasan saya mengatakan itu adalah karena saya pikir kebanyakan orang ketika mereka membaca ini dan jawaban yang serupa akan berpikir bahwa jika aliran mengandung "Hello"(tidak ada spasi spasi atau atau \n) dan std::stringdiekstraksi, itu akan mengekstrak surat dari Hke o, berhenti mengekstraksi, dan maka tidak mengatur bit EOF. Bahkan, itu akan mengatur bit EOF karena EOF yang menghentikan ekstraksi. Hanya berharap untuk menjernihkan hal itu bagi orang-orang.
Joseph Mansfield
103

Bottom-line top: Dengan penanganan ruang putih yang benar, berikut ini adalah bagaimana eofdapat digunakan (dan bahkan, lebih dapat diandalkan daripada fail()untuk pengecekan kesalahan):

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

( Terima kasih Tony D atas saran untuk menyoroti jawabannya. Lihat komentarnya di bawah ini untuk contoh mengapa ini lebih kuat. )


Argumen utama menentang penggunaan eof()tampaknya tidak ada seluk beluk penting tentang peran ruang putih. Proposisi saya adalah bahwa, memeriksa eof()secara eksplisit tidak hanya bukan " selalu salah " - yang tampaknya menjadi pendapat utama dalam hal ini dan utas SO yang serupa -, tetapi dengan penanganan ruang putih yang tepat, ini memberikan pembersih dan lebih dapat diandalkan penanganan kesalahan, dan merupakan solusi yang selalu benar (walaupun, belum tentu yang tersest).

Untuk meringkas apa yang disarankan sebagai penghentian dan baca "benar" adalah sebagai berikut:

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

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

Kegagalan karena upaya membaca di luar bukti diambil sebagai kondisi penghentian. Ini berarti bahwa tidak ada cara mudah untuk membedakan antara aliran yang berhasil dan yang benar-benar gagal karena alasan selain eof. Ambil aliran berikut:

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

while(in>>data)berakhir dengan satu set failbituntuk semua tiga masukan. Di bagian pertama dan ketiga, eofbitjuga diatur. Jadi melewati loop kita perlu logika ekstra yang sangat buruk untuk membedakan input yang tepat (1) dari yang tidak tepat (2 dan 3).

Sedangkan, ambil yang berikut:

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

Di sini, in.fail()memverifikasi bahwa selama ada sesuatu untuk dibaca, itu adalah yang benar. Tujuannya bukanlah terminator loop sementara.

Sejauh ini bagus, tapi apa yang terjadi jika ada ruang tambahan di sungai - apa yang terdengar seperti kekhawatiran utama eof()sebagai terminator?

Kami tidak perlu menyerahkan penanganan kesalahan kami; cukup makan ruang putih:

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::wsmelompati potensi (nol atau lebih) spasi tambahan di aliran saat mengatur eofbit, dan bukanfailbit . Jadi, in.fail()berfungsi seperti yang diharapkan, selama setidaknya ada satu data untuk dibaca. Jika aliran semua-kosong juga dapat diterima, maka formulir yang benar adalah:

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
}

Ringkasan: Dibuat dengan benar while(!eof)tidak hanya mungkin dan tidak salah, tetapi memungkinkan data untuk dilokalisasi dalam ruang lingkup, dan menyediakan pemisahan yang lebih bersih dari pengecekan kesalahan dari bisnis seperti biasa. Yang sedang berkata, while(!fail)mungkin idiom yang lebih umum dan singkat, dan mungkin lebih disukai dalam skenario sederhana (data tunggal per jenis baca).

licik
sumber
6
" Jadi melewati loop, tidak ada cara (mudah) untuk membedakan input yang tepat dari input yang tidak tepat. " Kecuali bahwa dalam satu kasus keduanya eofbitdan failbitdiatur, yang lain hanya failbitdiatur. Anda hanya perlu menguji bahwa sekali setelah loop telah berakhir, bukan pada setiap iterasi; itu hanya akan meninggalkan loop sekali, jadi Anda hanya perlu memeriksa mengapa itu meninggalkan loop sekali. while (in >> data)berfungsi dengan baik untuk semua aliran kosong.
Jonathan Wakely
3
Apa yang Anda katakan (dan poin yang dibuat sebelumnya) adalah bahwa aliran yang diformat buruk dapat diidentifikasi sebagai !eof & failloop masa lalu. Ada kasus di mana seseorang tidak bisa mengandalkan ini. Lihat komentar di atas ( goo.gl/9mXYX ). Bagaimanapun, saya tidak mengusulkan eof-ceck sebagai alternatif yang selalu lebih baik . Saya hanya mengatakan, itu adalah cara yang mungkin dan (dalam beberapa kasus lebih tepat) untuk melakukan ini, daripada "pasti salah!" karena cenderung diklaim di sini di SO.
licik
2
"Sebagai contoh, mempertimbangkan bagaimana Anda akan memeriksa kesalahan di mana data adalah struct dengan operator overload >> membaca beberapa bidang sekaligus" - kasus yang lebih sederhana mendukung titik Anda stream >> my_intdi mana sungai mengandung misalnya "-": eofbitdan failbityang set. Itu lebih buruk daripada operator>>skenario, di mana kelebihan yang disediakan pengguna setidaknya memiliki opsi untuk membersihkan eofbitsebelum kembali untuk membantu mendukung while (s >> x)penggunaan. Secara umum, jawaban ini bisa menggunakan pembersihan - hanya final while( !(in>>ws).eof() )yang kuat, dan akhirnya terkubur.
Tony Delroy
74

Karena jika programmer tidak menulis while(stream >> n), mereka mungkin menulis ini:

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

Di sini masalahnya adalah, Anda tidak dapat melakukannya some work on ntanpa terlebih dahulu memeriksa apakah aliran membaca berhasil, karena jika tidak berhasil, Anda some work on nakan menghasilkan hasil yang tidak diinginkan.

Seluruh titik adalah bahwa, eofbit, badbit, atau failbitditetapkan setelah dilakukan usaha untuk membaca dari sungai. Jadi jika stream >> ngagal, maka eofbit,, badbitatau failbitdiatur segera, jadi lebih idiomatis jika Anda menulis while (stream >> n), karena objek yang dikembalikan streamdikonversi ke falsejika ada beberapa kegagalan dalam membaca dari aliran dan akibatnya loop berhenti. Dan itu dikonversi menjadi truejika pembacaan berhasil dan perulangan berlanjut.

Nawaz
sumber
1
Terlepas dari "hasil yang tidak diinginkan" yang disebutkan dengan melakukan pekerjaan pada nilai yang tidak ditentukan n, program mungkin juga jatuh ke loop tak terbatas , jika operasi aliran yang gagal tidak mengkonsumsi input apa pun.
mastov
10

Jawaban lain telah menjelaskan mengapa logika salah while (!stream.eof())dan cara memperbaikinya. Saya ingin fokus pada sesuatu yang berbeda:

mengapa memeriksa bukti penggunaan iostream::eofsalah secara eksplisit ?

Secara umum, memeriksa eof hanya salah karena ekstraksi aliran ( >>) dapat gagal tanpa mengenai akhir file. Jika Anda memiliki eg int n; cin >> n;dan stream berisi hello, maka hitu bukan digit yang valid, jadi ekstraksi akan gagal tanpa mencapai akhir input.

Masalah ini, dikombinasikan dengan kesalahan logika umum saat memeriksa keadaan aliran sebelum mencoba membaca darinya, yang berarti untuk item input N loop akan berjalan N + 1 kali, mengarah ke gejala berikut:

  • Jika aliran kosong, loop akan berjalan sekali. >>akan gagal (tidak ada input untuk dibaca) dan semua variabel yang seharusnya disetel stream >> xsebenarnya tidak diinisialisasi. Ini mengarah pada data sampah yang sedang diproses, yang dapat bermanifestasi sebagai hasil yang tidak masuk akal (seringkali jumlahnya sangat besar).

    (Jika pustaka standar Anda sesuai dengan C ++ 11, segalanya agak berbeda sekarang: Yang gagal >>sekarang menetapkan variabel numerik 0alih-alih membiarkannya tidak diinisialisasi (kecuali untuk chars).)

  • Jika aliran tidak kosong, loop akan berjalan lagi setelah input yang valid terakhir. Karena dalam iterasi terakhir semua >>operasi gagal, variabel cenderung mempertahankan nilainya dari iterasi sebelumnya. Ini dapat bermanifestasi sebagai "baris terakhir dicetak dua kali" atau "catatan input terakhir diproses dua kali".

    (Ini seharusnya bermanifestasi sedikit berbeda sejak C ++ 11 (lihat di atas): Sekarang Anda mendapatkan "catatan hantu" dari nol alih-alih baris terakhir yang diulang.)

  • Jika aliran berisi data salah tetapi Anda hanya memeriksa .eof, Anda berakhir dengan loop tak terbatas. >>akan gagal mengekstraksi data apa pun dari arus, sehingga loop berputar di tempatnya tanpa pernah mencapai akhir.


Untuk rekap: Solusinya adalah untuk menguji keberhasilan >>operasi itu sendiri, tidak menggunakan terpisah .eof()metode: while (stream >> n >> m) { ... }, seperti di C Anda menguji keberhasilan scanfpanggilan itu sendiri: while (scanf("%d%d", &n, &m) == 2) { ... }.

melpomene
sumber
1
ini adalah jawaban yang paling akurat, meskipun pada c ++ 11, saya tidak percaya variabel tidak diinisialisasi lagi (pt bullet pertama)
csguy