Apa beberapa cara yang lebih baik untuk menghindari do-while (0); hack di C ++?

233

Ketika aliran kode seperti ini:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

Saya biasanya melihat pekerjaan ini untuk menghindari aliran kode yang berantakan di atas:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Apa sajakah cara yang lebih baik yang menghindari ini bekerja-sekitar / hack sehingga menjadi kode tingkat yang lebih tinggi (tingkat industri)?

Setiap saran yang ada di luar kotak dipersilahkan!

Sankalp
sumber
38
RAII dan lempar pengecualian.
ta.speot.is
135
Bagi saya, ini sepertinya waktu yang tepat untuk digunakan goto- tapi saya yakin seseorang akan menandai saya karena menyarankan itu, jadi saya tidak menulis jawaban untuk efek itu. Memiliki PANJANG do ... while(0);sepertinya adalah hal yang salah.
Mats Petersson
42
@dasblinkenlight: Ya, memang. Jika Anda akan menggunakannya goto, jujurlah tentang hal itu dan lakukan di tempat terbuka, jangan sembunyikan dengan menggunakan breakdando ... while
Mats Petersson
44
@MatsPetersson: Buat jawaban, saya akan memberi Anda +1 untuk gotoupaya rehabilitasi Anda . :)
wilx
27
@ ta.speot.is: "RAII dan lempar pengecualian". Dengan melakukan itu, Anda akan meniru kontrol aliran dengan pengecualian. Yaitu seperti menggunakan perangkat keras yang mahal dan berdarah sebagai palu atau pemberat kertas. Anda bisa melakukan itu, tetapi itu pasti rasanya sangat buruk bagi saya.
SigTerm

Jawaban:

309

Ini dianggap praktik yang dapat diterima untuk mengisolasi keputusan ini dalam suatu fungsi dan menggunakan returns bukannya breaks. Sementara semua pemeriksaan ini sesuai dengan tingkat abstraksi yang sama dengan fungsi, itu adalah pendekatan yang cukup logis.

Sebagai contoh:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}
Mikhail
sumber
22
@MatsPetersson: "Isolate in function" berarti refactoring ke fungsi baru yang hanya melakukan tes.
MSalters
35
+1. Ini juga jawaban yang bagus. Dalam C ++ 11, fungsi yang terisolasi dapat berupa lambda, karena ia juga dapat menangkap variabel lokal, jadi mempermudah semuanya!
Nawaz
4
@deworde: Bergantung pada situasi, solusi ini mungkin menjadi lebih lama dan lebih tidak dapat dibaca daripada goto. Karena C ++, sayangnya, tidak memungkinkan defintions fungsi lokal, Anda harus memindahkan fungsi itu (yang Anda akan kembali dari) di tempat lain, yang mengurangi keterbacaan. Mungkin berakhir dengan puluhan parameter, maka, karena memiliki puluhan parameter adalah hal yang buruk, Anda akan memutuskan untuk membungkusnya menjadi struct, yang akan membuat tipe data baru. Terlalu banyak mengetik untuk situasi sederhana.
SigTerm
24
@Damon jika ada, returnlebih bersih karena pembaca mana pun segera menyadari bahwa itu berfungsi dengan benar dan apa fungsinya. Dengan gotoAnda harus melihat sekeliling untuk melihat apa itu untuk, dan untuk memastikan bahwa tidak ada kesalahan yang dilakukan. Penyamaran adalah manfaatnya.
R. Martinho Fernandes
11
@SigTerm: Terkadang kode harus dipindahkan ke fungsi terpisah hanya untuk menjaga ukuran masing-masing fungsi tetap pada ukuran yang mudah dipikirkan. Jika Anda tidak ingin membayar panggilan fungsi, tandai forceinline.
Ben Voigt
257

Ada saat-saat ketika menggunakan gotosebenarnya jawaban yang BENAR - setidaknya bagi mereka yang tidak dibesarkan dalam kepercayaan agama bahwa " gototidak akan pernah bisa menjadi jawaban, tidak peduli apa pertanyaannya" - dan ini adalah salah satu kasus.

Kode ini menggunakan retasan do { ... } while(0);untuk tujuan berdandan gotoa break. Jika Anda akan menggunakan goto, maka terbuka tentang hal itu. Tidak ada gunanya membuat kode LEBIH KERAS untuk dibaca.

Situasi tertentu hanya ketika Anda memiliki banyak kode dengan kondisi yang cukup kompleks:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

Ini benar-benar dapat membuatnya lebih jelas bahwa kode itu benar dengan tidak memiliki logika yang rumit.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

Sunting: Untuk memperjelas, saya sama sekali tidak mengusulkan penggunaan gotosebagai solusi umum. Tetapi ada beberapa kasus di mana gotosolusi yang lebih baik daripada solusi lainnya.

Bayangkan misalnya kita sedang mengumpulkan beberapa data, dan kondisi berbeda yang sedang diuji adalah semacam "ini adalah akhir dari data yang dikumpulkan" - yang tergantung pada semacam penanda "lanjut / akhir" yang bervariasi tergantung di mana Anda berada di aliran data.

Sekarang, setelah selesai, kita perlu menyimpan data ke file.

Dan ya, sering ada solusi lain yang dapat memberikan solusi yang masuk akal, tetapi tidak selalu.

Mats Petersson
sumber
76
Tidak setuju. gotomungkin punya tempat, tetapi goto cleanuptidak. Pembersihan dilakukan dengan RAII.
MSalters
14
@ MSalters: Itu mengasumsikan bahwa pembersihan melibatkan sesuatu yang dapat diselesaikan dengan RAII. Mungkin saya seharusnya mengatakan "masalah kesalahan" atau semacamnya.
Mats Petersson
25
Jadi, teruslah mendapatkan suara yang satu ini, mungkin dari orang-orang yang beragama gototidak pernah menjadi jawaban yang tepat. Saya akan menghargai jika ada komentar ...
Mats Petersson
19
orang benci gotokarena Anda harus berpikir untuk menggunakannya / untuk memahami suatu program yang menggunakannya ... Mikroprosesor, di sisi lain, dibangun jumpsdan conditional jumps... jadi masalahnya ada pada beberapa orang, bukan dengan logika atau hal lain.
woliveirajr
17
+1 untuk penggunaan yang tepat dari goto. RAII bukanlah solusi yang tepat jika kesalahan diharapkan (tidak luar biasa), karena itu akan menjadi penyalahgunaan pengecualian.
Joe
82

Anda dapat menggunakan pola kelanjutan sederhana dengan boolvariabel:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

Rantai eksekusi ini akan berhenti segera setelah checkNpengembalian a false. Tidak ada check...()panggilan lebih lanjut yang akan dilakukan karena hubungan arus pendek dari &&operator. Selain itu, mengoptimalkan kompiler yang cukup pintar untuk mengenali pengaturan yang goOnke falseadalah jalan satu arah, dan masukkan hilang goto enduntuk Anda. Akibatnya, kinerja kode di atas akan identik dengan a do/ while(0), hanya tanpa pukulan menyakitkan untuk keterbacaannya.

dasblinkenlight
sumber
30
Tugas dalam ifkondisi terlihat sangat mencurigakan.
Mikhail
90
@Mikhail Hanya untuk mata yang tidak terlatih.
dasblinkenlight
20
Saya telah menggunakan teknik ini sebelumnya dan itu selalu semacam menyadap saya bahwa saya pikir kompiler harus menghasilkan kode untuk memeriksa di setiap iftidak peduli apa goOnyang bahkan jika yang awal gagal (sebagai lawan melompat / pecah) ... tapi saya baru saja menjalankan tes dan VS2012 setidaknya cukup pintar untuk melakukan hubungan arus pendek setelah kesalahan pertama. Saya akan menggunakan ini lebih sering. Catatan: jika Anda menggunakan goOn &= checkN()maka checkN()akan selalu berjalan bahkan jika goOnitu falsedi awal if(yaitu, jangan lakukan itu).
tandai
11
@Nawaz: Jika Anda memiliki pikiran yang tidak terlatih membuat perubahan sewenang-wenang di seluruh basis kode, Anda memiliki masalah yang jauh lebih besar daripada hanya tugas di dalam ifs.
Idelic
6
@sisharp Elegance begitu di mata yang melihatnya! Saya gagal untuk memahami bagaimana di dunia ini penyalahgunaan konstruksi perulangan bisa dianggap "elegan", tapi mungkin itu hanya saya.
dasblinkenlight
38
  1. Cobalah untuk mengekstrak kode menjadi fungsi yang terpisah (atau mungkin lebih dari satu). Kemudian kembali dari fungsi jika pemeriksaan gagal.

  2. Jika terlalu erat dengan kode di sekitarnya untuk melakukan itu, dan Anda tidak dapat menemukan cara untuk mengurangi kopling, lihat kode setelah blok ini. Agaknya, itu membersihkan beberapa sumber daya yang digunakan oleh fungsi. Cobalah untuk mengelola sumber daya ini menggunakan objek RAII ; kemudian ganti setiap dodgy breakdengan return(atau throw, jika itu lebih tepat) dan biarkan destructor objek bersih untuk Anda.

  3. Jika aliran program (harus) begitu berlekuk sehingga Anda benar-benar membutuhkannya goto, maka gunakan itu alih-alih memberikan penyamaran yang aneh.

  4. Jika Anda memiliki aturan pengkodean yang dilarang secara membabi buta goto, dan Anda benar-benar tidak dapat menyederhanakan aliran program, maka Anda mungkin harus menyamarkannya dengan dohack.

Mike Seymour
sumber
4
Dengan rendah hati saya serahkan bahwa RAII, meskipun bermanfaat, bukanlah peluru perak. Ketika Anda menemukan diri Anda akan menulis kelas convert-goto-to-RAII yang tidak memiliki kegunaan lain, saya pasti berpikir Anda akan lebih baik dilayani hanya dengan menggunakan idiom "goto-end-of-the-dunia" yang disebutkan orang-orang.
idoby
1
@busy_wait: Memang, RAII tidak bisa menyelesaikan semuanya; itu sebabnya jawaban saya tidak berhenti pada poin kedua, tetapi terus menyarankan gotojika itu benar-benar pilihan yang lebih baik.
Mike Seymour
3
Saya setuju, tapi saya pikir itu ide buruk untuk menulis kelas konversi goto-RAII dan saya pikir itu harus dinyatakan secara eksplisit.
idoby
@ idoby bagaimana dengan menulis satu kelas template RAII, dan instantiate dengan pembersihan penggunaan tunggal apa pun yang Anda butuhkan dalam lambda
Caleth
@Caleth terdengar bagi saya seperti Anda sedang menciptakan kembali ctors / dtors?
idoby
37

TLDR : RAII , kode transaksional (hanya mengatur hasil atau mengembalikan barang saat sudah dihitung) dan pengecualian.

Jawaban panjang:

Dalam C , praktik terbaik untuk kode semacam ini adalah dengan menambahkan label EXIT / CLEANUP / lainnya dalam kode, tempat pembersihan sumber daya lokal terjadi dan kode kesalahan (jika ada) dikembalikan. Ini adalah praktik terbaik karena memecah kode secara alami menjadi inisialisasi, perhitungan, komit dan kembali:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

Dalam C, di sebagian codebases, yang if(error_ok != ...dan gotokode biasanya tersembunyi di balik beberapa macro kenyamanan ( RET(computation_result), ENSURE_SUCCESS(computation_result, return_code), dll).

C ++ menawarkan alat tambahan lebih dari C :

  • Fungsi blok pembersihan dapat diimplementasikan sebagai RAII, yang berarti Anda tidak lagi memerlukan seluruh cleanupblok dan memungkinkan kode klien untuk menambahkan pernyataan pengembalian awal.

  • Anda melempar kapan pun Anda tidak bisa melanjutkan, mengubah semua itu if(error_ok != ...menjadi panggilan langsung.

Kode C ++ Setara:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

Ini adalah praktik terbaik karena:

  • Itu eksplisit (yaitu, sementara penanganan kesalahan tidak eksplisit, aliran utama algoritma adalah)

  • Sangat mudah untuk menulis kode klien

  • Minimal

  • Sederhana saja

  • Ini tidak memiliki konstruksi kode berulang

  • Tidak menggunakan makro

  • Itu tidak menggunakan do { ... } while(0)konstruksi aneh

  • Ini dapat digunakan kembali dengan sedikit usaha (yaitu, jika saya ingin menyalin panggilan ke computation2();fungsi yang berbeda, saya tidak harus memastikan saya menambahkan do { ... } while(0)kode baru, atau #definemakro pembungkus goto dan label pembersihan, atau ada yang lain).

utnapistim
sumber
+1. Inilah yang saya coba gunakan, menggunakan RAII atau sesuatu. shared_ptrdengan custom deleter dapat melakukan banyak hal. Lebih mudah dengan lambdas di C ++ 11.
Macke
Benar, tetapi jika Anda akhirnya menggunakan shared_ptr untuk hal-hal yang bukan pointer, pertimbangkan setidaknya mengetikkannya : namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };Dalam hal ini (dengan make_handlemenetapkan tipe deleter yang benar pada konstruksi), nama tipe tidak lagi menunjukkan itu adalah pointer .
utnapistim
21

Saya menambahkan jawaban demi kelengkapan. Sejumlah jawaban lain menunjukkan bahwa blok kondisi besar dapat dipecah menjadi fungsi yang terpisah. Tetapi seperti yang telah ditunjukkan beberapa kali adalah bahwa pendekatan ini memisahkan kode kondisional dari konteks aslinya. Ini adalah salah satu alasan mengapa lambdas ditambahkan ke bahasa dalam C ++ 11. Menggunakan lambdas disarankan oleh orang lain tetapi tidak ada sampel eksplisit yang diberikan. Saya telah memasukkan satu dalam jawaban ini. Apa yang mengejutkan saya adalah bahwa itu terasa sangat mirip dengan do { } while(0)pendekatan dalam banyak hal - dan mungkin itu berarti masih gotomenyamar ....

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations
Peter R
sumber
7
Bagi saya hack ini terlihat lebih buruk daripada do ... saat hack.
Michael
18

Tentu saja tidak dengan jawaban, tapi jawaban (demi kelengkapan)

Dari pada :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Anda bisa menulis:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Ini masih merupakan kebohongan yang terselubung, tetapi setidaknya ini bukan loop lagi. Yang berarti Anda tidak perlu hati-hati memeriksa tidak ada beberapa yang terus disembunyikan di suatu tempat di blok.

Konstruksinya juga cukup sederhana sehingga Anda bisa berharap kompiler akan mengoptimalkannya.

Seperti yang disarankan oleh @jamesdlin, Anda bahkan dapat menyembunyikan itu di belakang seperti makro

#define BLOC switch(0) case 0:

Dan gunakan itu seperti

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Ini dimungkinkan karena sintaks bahasa C mengharapkan pernyataan setelah beralih, bukan blok yang ditandai dan Anda dapat menempatkan label kasus sebelum pernyataan itu. Sampai sekarang saya tidak melihat gunanya membiarkan itu, tetapi dalam kasus khusus ini berguna untuk menyembunyikan saklar di balik makro yang bagus.

Kriss
sumber
2
Ooh, pintar. Anda bahkan bisa menyembunyikannya di balik makro suka define BLOCK switch (0) case 0:dan menggunakannya suka BLOCK { ... break; }.
jamesdlin
@jamesdlin: tidak pernah terpikir oleh saya sebelumnya bahwa ini bisa berguna untuk meletakkan case sebelum braket pembuka switch. Tetapi memang diizinkan oleh C dan dalam hal ini berguna untuk menulis makro yang bagus.
Kriss
15

Saya akan merekomendasikan pendekatan yang mirip dengan jawaban Mats dikurangi yang tidak perlu goto. Hanya menempatkan logika kondisional dalam fungsi. Kode apa pun yang selalu berjalan harus masuk sebelum atau setelah fungsi dipanggil dalam pemanggil:

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}
Dan Bechard
sumber
3
Jika Anda harus mendapatkan sumber daya lain di tengah func, Anda perlu faktor-out fungsi lain (per pola Anda). Jika semua fungsi yang terisolasi ini membutuhkan data yang sama, Anda hanya akan menyalin argumen tumpukan yang sama berulang-ulang, atau memutuskan untuk mengalokasikan argumen Anda di heap dan meneruskan sebuah pointer di sekitar, tidak memanfaatkan fitur bahasa yang paling dasar ( argumen fungsi). Singkatnya, saya tidak percaya solusi ini menskala untuk kasus terburuk di mana setiap kondisi diperiksa setelah mendapatkan sumber daya baru yang harus dibersihkan. Perhatikan komentar Nawaz tentang lambdas.
mentega
2
Saya tidak ingat OP mengatakan apa-apa tentang mendapatkan sumber daya di tengah-tengah blok kode-nya, tapi saya menerimanya sebagai persyaratan yang layak. Dalam hal ini apa yang salah dengan mendeklarasikan sumber daya di stack di mana saja func()dan membiarkan destruktornya menangani sumber daya yang bebas? Jika ada yang di luar dari func()kebutuhan akses ke sumber daya yang sama itu harus dinyatakan pada heap sebelum memanggil func()oleh manajer sumber daya yang tepat.
Dan Bechard
12

Aliran kode itu sendiri sudah merupakan bau kode yang banyak terjadi dalam fungsi. Jika tidak ada solusi langsung untuk itu (fungsinya adalah fungsi pemeriksaan umum), maka gunakan RAII sehingga Anda dapat kembali alih-alih melompat ke bagian akhir fungsi mungkin lebih baik.

stefaanv
sumber
11

Jika Anda tidak perlu memperkenalkan variabel lokal selama eksekusi maka Anda dapat sering meratakan ini:

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()
the_mandrill
sumber
2
Tetapi kemudian setiap kondisi harus memasukkan yang sebelumnya, yang tidak hanya jelek tetapi berpotensi buruk untuk kinerja. Lebih baik melompati cek dan pembersihan itu segera setelah kita tahu kita "tidak baik-baik saja".
mentega
Itu benar, namun pendekatan ini memang memiliki keuntungan jika ada hal-hal yang harus dilakukan di akhir sehingga Anda tidak ingin kembali lebih awal (lihat edit). Jika Anda menggabungkan dengan pendekatan Denise menggunakan bools dalam kondisi, maka performa hit akan diabaikan kecuali ini dalam loop yang sangat ketat.
the_mandrill
10

Mirip dengan jawaban dasblinkenlight, tetapi menghindari tugas di dalam ifyang bisa diperbaiki oleh pengkaji kode:

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

Saya menggunakan pola ini ketika hasil suatu langkah perlu diperiksa sebelum langkah berikutnya, yang berbeda dari situasi di mana semua pemeriksaan dapat dilakukan di depan dengan if( check1() && check2()...pola tipe besar .

Denise Skidmore
sumber
Perhatikan bahwa check1 () benar-benar bisa menjadi PerformStep1 () yang mengembalikan kode hasil langkah tersebut. Ini akan mengurangi kompleksitas fungsi aliran proses Anda.
Denise Skidmore
10

Gunakan pengecualian. Kode Anda akan terlihat jauh lebih bersih (dan pengecualian dibuat untuk menangani kesalahan dalam alur eksekusi suatu program). Untuk membersihkan sumber daya (deskriptor file, koneksi database, dll,), baca artikel Mengapa C ++ tidak menyediakan konstruksi "akhirnya"? .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}
Cartucho
sumber
10
Betulkah? Pertama, jujur ​​saya tidak melihat peningkatan keterbacaan di sana. JANGAN GUNAKAN PENGECUALIAN UNTUK MENGENDALIKAN ALIRAN PROGRAM. Itu BUKAN tujuan mereka yang semestinya. Juga, pengecualian membawa penalti kinerja yang signifikan. Kasus penggunaan yang tepat untuk pengecualian adalah ketika beberapa kondisi hadir BAHWA ANDA TIDAK BISA MELAKUKAN APA SAJA TENTANG, seperti mencoba membuka file yang sudah dikunci secara eksklusif oleh proses lain, atau koneksi jaringan gagal, atau panggilan ke database gagal , atau pemanggil meneruskan parameter yang tidak valid ke suatu prosedur. Hal semacam itu. Tapi JANGAN gunakan pengecualian untuk mengontrol aliran program.
Craig
3
Maksud saya, bukan untuk menjadi ingus tentang hal itu, tetapi pergi ke artikel Stroustrup yang Anda referensikan dan cari "Apa yang tidak boleh saya gunakan untuk pengecualian?" bagian. Antara lain, ia mengatakan: "Secara khusus, melempar bukan hanya cara alternatif untuk mengembalikan nilai dari fungsi (mirip dengan kembali). Melakukannya akan lambat dan akan membingungkan sebagian besar programmer C ++ yang digunakan untuk mencari pengecualian yang hanya digunakan untuk kesalahan penanganan. Demikian pula, melempar bukanlah cara yang baik untuk keluar dari lingkaran. "
Craig
3
@Craig Semua yang Anda tunjuk benar, tetapi Anda berasumsi bahwa program sampel dapat dilanjutkan setelah check()kondisi gagal, dan itu adalah asumsi ANDA, tidak ada konteks dalam sampel. Dengan asumsi bahwa program tidak dapat dilanjutkan, menggunakan pengecualian adalah cara yang harus dilakukan.
Cartucho
2
Yah, cukup benar jika itu sebenarnya konteksnya. Tapi, hal lucu tentang asumsi ... ;-)
Craig
10

Bagi saya do{...}while(0)itu baik-baik saja. Jika Anda tidak ingin melihatnya do{...}while(0), Anda dapat menentukan kata kunci alternatif untuk mereka.

Contoh:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;

   //processing code here

END_TEST

Saya pikir kompiler akan menghapus while(0)kondisi yang tidak perlu di Windowsdo{...}while(0) dalam versi biner dan mengubah istirahat menjadi lompatan tanpa syarat. Anda dapat memastikan versi bahasa assemblynya untuk memastikan.

Menggunakan gotojuga menghasilkan kode yang lebih bersih dan langsung dengan logika kondisi-kemudian-lompat. Anda dapat melakukan hal berikut:

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;

   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.

Perhatikan label ditempatkan setelah penutupan }. Ini adalah salah satu masalah yang mungkin terjadi gotokarena secara tidak sengaja menempatkan kode di antaranya karena Anda tidak melihat label. Sekarang seperti do{...}while(0)tanpa kode kondisi.

Untuk membuat kode ini lebih bersih dan lebih mudah dipahami, Anda dapat melakukan ini:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

Dengan ini, Anda dapat melakukan blok bersarang dan menentukan di mana Anda ingin keluar / melompat.

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);

      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

Kode spaghetti bukan kesalahan goto, itu kesalahan programmer. Anda masih dapat menghasilkan kode spageti tanpa menggunakan goto.

acegs
sumber
10
Saya akan memilih gotolebih dari memperpanjang sintaks bahasa menggunakan preprocessor satu juta kali lipat.
Christian
2
"Untuk membuat kode ini lebih bersih dan lebih mudah dipahami, Anda dapat [menggunakan LOADS_OF_WEIRD_MACROS]" : tidak menghitung.
underscore_d
8

Ini adalah masalah yang terkenal dan diselesaikan dari perspektif pemrograman fungsional - mungkin monad.

Menanggapi komentar yang saya terima di bawah ini, saya telah mengedit pengantar saya di sini: Anda dapat menemukan detail lengkap tentang penerapan C ++ monads di berbagai tempat yang memungkinkan Anda mencapai apa yang disarankan Rotsor. Butuh beberapa saat untuk grok monads jadi alih-alih saya akan menyarankan di sini mekanisme cepat "orang miskin" yang mirip monad yang perlu Anda ketahui tentang tidak lebih dari boost :: opsional.

Siapkan langkah perhitungan Anda sebagai berikut:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

Setiap langkah komputasi jelas dapat melakukan sesuatu seperti mengembalikan boost::nonejika opsi yang diberikannya kosong. Jadi misalnya:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

Kemudian rangkai mereka:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

Yang menyenangkan tentang ini adalah bahwa Anda dapat menulis unit test yang terdefinisi dengan jelas untuk setiap langkah komputasi. Doa juga berbunyi seperti bahasa Inggris biasa (seperti biasanya dengan gaya fungsional).

Jika Anda tidak peduli tentang kekekalan dan lebih mudah untuk mengembalikan objek yang sama setiap kali Anda dapat membuat beberapa variasi menggunakan shared_ptr atau sejenisnya.

Benediktus
sumber
3
Kode ini memiliki properti yang tidak diinginkan memaksa setiap fungsi individu untuk menangani kegagalan fungsi sebelumnya, sehingga tidak memanfaatkan idiom Monad (di mana efek monadik, dalam hal ini kegagalan, seharusnya ditangani secara implisit). Untuk melakukan itu, Anda harus memiliki optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);dan menggunakan operasi komposisi monadik ('bind') daripada aplikasi fungsi.
Rotsor
Terima kasih. Anda benar - itulah cara melakukannya dengan benar. Saya tidak ingin menulis terlalu banyak dalam jawaban saya untuk menjelaskan bahwa (karena itu istilah "orang miskin" yang dimaksudkan untuk menyarankan saya tidak pergi seluruh babi di sini).
Benediktus
7

Bagaimana dengan memindahkan pernyataan if ke fungsi ekstra yang menghasilkan hasil numerik atau enum?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}
karx11erx
sumber
Ini bagus bila memungkinkan, tetapi jauh lebih umum daripada pertanyaan yang diajukan di sini. Setiap kondisi dapat bergantung pada kode yang dieksekusi setelah tes debranching terakhir.
Kriss
Masalahnya di sini adalah Anda memisahkan sebab dan akibat. Yaitu Anda memisahkan kode yang mengacu pada nomor kondisi yang sama dan ini dapat menjadi sumber bug lebih lanjut.
Riga
@ Kriss: Yah, fungsi ConditionCode () dapat disesuaikan untuk mengurus ini. Poin utama dari fungsi itu adalah Anda dapat menggunakan return <result> untuk jalan keluar yang bersih segera setelah kondisi akhir telah dihitung; Dan itulah yang memberikan kejelasan struktural di sini.
karx11erx
@ Riga: Saya ini benar-benar keberatan akademis. Saya menemukan C ++ menjadi lebih kompleks, samar dan tidak dapat dibaca dengan setiap versi baru. Saya tidak bisa melihat masalah dengan fungsi pembantu kecil mengevaluasi kondisi kompleks dengan cara yang terstruktur dengan baik untuk membuat fungsi target tepat di bawah ini lebih mudah dibaca.
karx11erx
1
@ karx11erx keberatan saya praktis dan berdasarkan pengalaman saya. Pola ini buruk tidak terkait dengan C ++ 11 atau bahasa apa pun. Jika Anda memiliki kesulitan dengan konstruksi bahasa yang memungkinkan Anda untuk menulis arsitektur yang baik, itu bukan masalah bahasa.
Riga
5

Sesuatu seperti ini mungkin

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

atau gunakan pengecualian

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

Menggunakan pengecualian, Anda juga dapat meneruskan data.

Liftarn
sumber
10
Tolong jangan lakukan hal-hal pintar seperti definisi Anda sebelumnya, mereka biasanya membuat kode lebih sulit dibaca untuk sesama pengembang. Saya telah melihat seseorang mendefinisikan Case sebagai break; case dalam file header, dan menggunakannya dalam switch dalam file cpp, membuat orang lain bertanya-tanya selama berjam-jam mengapa switch rusak di antara pernyataan Case. Grrr ...
Michael
5
Dan ketika Anda memberi nama makro, Anda harus membuatnya seperti makro (yaitu, dalam semua huruf besar). Kalau tidak, seseorang yang kebetulan memberi nama variabel / fungsi / tipe / dll. namanya everakan sangat tidak bahagia ...
jamesdlin
5

Saya tidak terlalu suka menggunakan breakatau returndalam kasus seperti itu. Mengingat bahwa biasanya ketika kita menghadapi situasi seperti itu, itu biasanya merupakan metode yang relatif panjang.

Jika kita memiliki beberapa titik keluar, itu dapat menyebabkan kesulitan ketika kita ingin tahu apa yang akan menyebabkan logika tertentu untuk dieksekusi: Biasanya kita terus naik blok melampirkan potongan logika itu, dan kriteria dari blok penutup memberitahu kita situasi:

Sebagai contoh,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

Dengan melihat melampirkan blok, mudah untuk mengetahui bahwa myLogic()hanya terjadi kapanconditionA and conditionB and conditionC itu benar.

Itu menjadi jauh kurang terlihat ketika ada pengembalian awal:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

Kami tidak dapat lagi menavigasi dari myLogic() , melihat melampirkan blok untuk mengetahui kondisi.

Ada beberapa solusi yang saya gunakan. Ini salah satunya:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(Tentu saja disambut menggunakan variabel yang sama untuk menggantikan semua isA isB isC .)

Pendekatan semacam itu setidaknya akan memberikan pembaca kode, yang myLogic()dieksekusi ketika isB && conditionC. Pembaca diberi petunjuk bahwa ia perlu mencari lebih jauh apa yang menyebabkan isB menjadi benar.

Adrian Shum
sumber
3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

Bagaimana tentang itu?

sniperbat
sumber
jika sudah usang, adil return condition;, kalau tidak saya pikir ini bisa dipertahankan.
SpaceTrucker
2

Pola lain yang berguna jika Anda memerlukan langkah-langkah pembersihan berbeda tergantung di mana kegagalannya:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }
Denise Skidmore
sumber
2

Saya bukan programmer C ++ , jadi saya tidak akan menulis kode apa pun di sini, tapi sejauh ini tidak ada yang menyebutkan solusi berorientasi objek. Jadi, inilah tebakan saya tentang itu:

Memiliki antarmuka generik yang menyediakan metode untuk mengevaluasi satu kondisi. Sekarang Anda dapat menggunakan daftar implementasi dari kondisi-kondisi di objek Anda yang berisi metode yang dimaksud. Anda mengulangi daftar dan mengevaluasi setiap kondisi, mungkin keluar awal jika salah satu gagal.

Yang baik adalah bahwa desain seperti itu menempel dengan sangat baik pada prinsip terbuka / tertutup , karena Anda dapat dengan mudah menambahkan kondisi baru selama inisialisasi objek yang berisi metode yang dimaksud. Anda bahkan dapat menambahkan metode kedua ke antarmuka dengan metode untuk evaluasi kondisi mengembalikan deskripsi kondisi. Ini dapat digunakan untuk sistem dokumentasi diri.

The downside, bagaimanapun, adalah bahwa ada sedikit lebih banyak overhead yang terlibat karena penggunaan lebih banyak objek dan iterasi di atas daftar.

SpaceTrucker
sumber
Bisakah Anda menambahkan contoh dalam bahasa yang berbeda? Saya pikir pertanyaan ini berlaku untuk banyak bahasa meskipun ditanya khusus tentang C ++.
Denise Skidmore
1

Ini adalah cara saya melakukannya.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}
Vadoff
sumber
1

Pertama, contoh singkat untuk menunjukkan mengapa gotoini bukan solusi yang baik untuk C ++:

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

Cobalah untuk mengkompilasi ini menjadi file objek dan lihat apa yang terjadi. Lalu coba yang setara do+ break+while(0) .

Itu adalah samping. Poin utama berikut.

Mereka potongan kecil kode sering membutuhkan beberapa jenis pembersihan harus seluruh fungsi gagal. Pembersihan itu biasanya ingin terjadi dalam urutan yang berlawanan dari potongan itu sendiri, ketika Anda "melepaskan" perhitungan yang sebagian selesai.

Salah satu opsi untuk mendapatkan semantik ini adalah RAII ; lihat jawaban @ utnapistim. C ++ menjamin bahwa destruktor otomatis berjalan dalam urutan yang berlawanan dengan konstruktor, yang secara alami memasok "pelepasan".

Tapi itu membutuhkan banyak kelas RAII. Terkadang opsi yang lebih sederhana adalah dengan menggunakan stack:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...dan seterusnya. Ini mudah untuk diaudit, karena menempatkan kode "undo" di sebelah kode "do". Audit mudah itu bagus. Itu juga membuat aliran kontrol sangat jelas. Ini juga merupakan pola yang berguna untuk C.

Mungkin diperlukan calcfungsi untuk mengambil banyak argumen, tetapi itu biasanya tidak masalah jika kelas / struct Anda memiliki kohesi yang baik. (Yaitu, barang-barang yang dimiliki bersama hidup dalam satu objek tunggal, sehingga fungsi-fungsi ini dapat mengambil pointer atau referensi ke sejumlah kecil objek dan masih melakukan banyak pekerjaan yang bermanfaat.)

Nemo
sumber
Sangat mudah untuk mengaudit jalur pembersihan, tetapi mungkin tidak begitu mudah untuk melacak jalur emas. Tapi secara keseluruhan, saya pikir sesuatu seperti ini memang mendorong pola pembersihan yang konsisten.
Denise Skidmore
0

Jika kode Anda memiliki blok panjang if..else if..else pernyataan, Anda dapat mencoba dan menulis ulang seluruh blok dengan bantuan Functorsatau function pointers. Ini mungkin bukan solusi yang tepat selalu tetapi cukup sering.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html

HAL
sumber
Pada prinsipnya ini mungkin (dan tidak buruk), tetapi dengan objek atau-fungsi fungsi eksplisit itu hanya mengganggu aliran kode terlalu kuat. SELAIN itu, di sini setara, penggunaan lambdas atau fungsi yang disebut biasa adalah praktik yang baik, efisien dan membaca dengan baik.
leftaroundabout
0

Saya kagum dengan banyaknya jawaban berbeda yang disajikan di sini. Tapi, akhirnya dalam kode yang harus saya ubah (yaitu menghapus do-while(0)hack ini atau apa pun), saya melakukan sesuatu yang berbeda dari jawaban yang disebutkan di sini dan saya bingung mengapa tidak ada yang memikirkan hal ini. Inilah yang saya lakukan:

Kode awal:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

Sekarang:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

Jadi, apa yang telah dilakukan di sini adalah bahwa barang-barang finishing telah diisolasi dalam suatu fungsi dan hal-hal tiba-tiba menjadi sangat sederhana dan bersih!

Saya pikir solusi ini layak disebutkan, jadi berikan di sini.

Sankalp
sumber
0

Konsolidasikan ke dalam satu ifpernyataan:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

Ini adalah pola yang digunakan dalam bahasa seperti Jawa tempat kata kunci goto dihapus.

Tyzoid
sumber
2
Itu hanya berfungsi jika Anda tidak perlu melakukan apa pun di antara tes kondisi. (well, saya kira Anda bisa menyembunyikan hal-hal di dalam beberapa pemanggilan fungsi yang dilakukan oleh tes-kondisi, tetapi itu mungkin sedikit membingungkan jika Anda melakukannya terlalu banyak)
Jeremy Friesner
@JeremyFriesner Sebenarnya, Anda benar-benar bisa melakukan di-antara-hal sebagai fungsi boolean terpisah yang selalu dievaluasi sebagai true. Evaluasi hubung singkat akan menjamin bahwa Anda tidak pernah menjalankan hal-hal di antaranya yang tidak lulus semua tes prasyarat.
AJMansfield
@ AJMansfield ya, itulah yang saya maksudkan dalam kalimat kedua saya ... tapi saya tidak yakin melakukan itu akan menjadi peningkatan kualitas kode.
Jeremy Friesner
@JeremyFriesner Tidak ada yang menghentikan Anda dari menulis persyaratan (/*do other stuff*/, /*next condition*/), Anda bahkan dapat memformatnya dengan baik. Hanya saja, jangan berharap orang menyukainya. Tapi jujur, ini hanya untuk menunjukkan bahwa itu adalah kesalahan bagi Jawa untuk menghapus gotopernyataan itu ...
cmaster - mengembalikan monica
@ JeremyFriesner Saya berasumsi bahwa ini adalah boolean. Jika fungsi dijalankan di dalam setiap kondisi, maka ada cara yang lebih baik untuk menanganinya.
Tyzoid
0

Jika menggunakan penangan kesalahan yang sama untuk semua kesalahan, dan setiap langkah mengembalikan bool yang menunjukkan keberhasilan:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(Mirip dengan jawaban tyzoid, tetapi kondisinya adalah tindakan, dan && mencegah tindakan tambahan terjadi setelah kegagalan pertama.)

Denise Skidmore
sumber
0

Mengapa metode penandaan tidak menjawabnya digunakan sejak lama.

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
Fennekin
sumber