Penggunaan goto yang valid untuk manajemen kesalahan di C?

95

Pertanyaan ini sebenarnya adalah hasil diskusi menarik di programming.reddit.com beberapa waktu lalu. Ini pada dasarnya bermuara pada kode berikut:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Penggunaan di gotosini tampaknya menjadi cara terbaik untuk pergi, menghasilkan kode paling bersih dan paling efisien dari semua kemungkinan, atau setidaknya begitulah menurut saya. Mengutip Steve McConnell dalam Code Complete :

Goto berguna dalam rutinitas yang mengalokasikan sumber daya, melakukan operasi pada sumber daya tersebut, dan kemudian membatalkan alokasi sumber daya. Dengan goto, Anda dapat membersihkan di satu bagian kode. Goto mengurangi kemungkinan Anda lupa untuk membatalkan alokasi sumber daya di setiap tempat Anda mendeteksi kesalahan.

Dukungan lain untuk pendekatan ini datang dari buku Linux Device Drivers , di bagian ini .

Bagaimana menurut anda? Apakah kasus ini digunakan secara valid untuk gotodi C? Apakah Anda lebih suka metode lain, yang menghasilkan kode yang lebih berbelit-belit dan / atau kurang efisien, tetapi hindari goto?

Eli Bendersky
sumber
@Eli: Mengapa Anda tidak menghapus tag dan menempatkan fungsi (cleanup_3 ();) di dalam tanda kurung if?
@ Akito: apa maksudmu? Bisakah Anda memposting saran Anda sebagai jawaban dengan sampel kode lengkap?
Eli Bendersky
@EliBendersky: Silakan lihat jawaban saya.
Salah satu hal yang paling saya benci dari Visual Basic (termasuk VBS dan VB6) adalah on error goto errorsistem penanganan kesalahan :)
Manu343726

Jawaban:

64

FWIF, saya menemukan idiom penanganan kesalahan yang Anda berikan dalam contoh pertanyaan menjadi lebih mudah dibaca dan lebih mudah dipahami daripada alternatif apa pun yang diberikan dalam jawaban sejauh ini. Meskipun gotosecara umum merupakan ide yang buruk, ini dapat berguna untuk penanganan kesalahan jika dilakukan dengan cara yang sederhana dan seragam. Dalam situasi ini, meskipun itu a goto, itu digunakan dengan cara yang didefinisikan dengan baik dan lebih atau kurang terstruktur.

Michael Burr
sumber
1
Tidak bisakah dia menghapus tag tersebut dan meletakkan fungsinya langsung ke if? Bukankah akan lebih mudah dibaca?
8
@StartupCrazy, saya tahu ini sudah bertahun-tahun, tetapi demi validitas posting di situs ini saya akan menunjukkan bahwa tidak, dia tidak bisa. Jika dia mendapat kesalahan di goto error3 dalam kodenya, dia akan menjalankan pembersihan 1 2 dan 3, dalam solusi yang disarankan dia hanya akan menjalankan pembersihan 3. Dia bisa menyarangkan sesuatu, tetapi itu hanya akan menjadi panah antipattern, sesuatu yang harus dihindari oleh programmer .
gbtimmon
18

Sebagai aturan umum, menghindari goto adalah ide yang bagus, tetapi pelanggaran yang lazim ketika Dijkstra pertama kali menulis 'GOTO Dianggap Berbahaya' bahkan tidak terlintas dalam pikiran kebanyakan orang sebagai pilihan akhir-akhir ini.

Apa yang Anda garis besarkan adalah solusi yang dapat digeneralisasikan untuk masalah penanganan kesalahan - tidak masalah bagi saya selama digunakan dengan hati-hati.

Contoh khusus Anda dapat disederhanakan sebagai berikut (langkah 1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Melanjutkan proses:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

Ini, saya yakin, sama dengan kode aslinya. Ini terlihat sangat bersih karena kode aslinya sangat bersih dan terorganisir dengan baik. Seringkali, fragmen kode tidak serapi itu (meskipun saya menerima argumen yang seharusnya); misalnya, sering kali ada lebih banyak status untuk diteruskan ke rutinitas inisialisasi (setup) daripada yang ditunjukkan, dan karena itu lebih banyak status untuk diteruskan ke rutinitas pembersihan juga.

Jonathan Leffler
sumber
24
Ya, solusi bersarang adalah salah satu alternatif yang layak. Sayangnya, ini menjadi kurang layak saat level bersarang semakin dalam.
Eli Bendersky
4
@eliben: Setuju - tetapi penumpukan yang lebih dalam mungkin (mungkin adalah) indikasi bahwa Anda perlu memperkenalkan lebih banyak fungsi, atau meminta langkah-langkah persiapan melakukan lebih banyak, atau sebaliknya mengubah kode Anda. Saya dapat berargumen bahwa masing-masing fungsi persiapan harus melakukan penyiapannya, memanggil yang berikutnya dalam rantai, dan melakukan pembersihannya sendiri. Ini melokalkan pekerjaan itu - Anda bahkan dapat menghemat tiga fungsi pembersihan. Hal ini juga tergantung, sebagian, pada apakah salah satu fungsi penyiapan atau pembersihan digunakan (dapat digunakan) dalam urutan panggilan lainnya.
Jonathan Leffler
6
Sayangnya ini tidak bekerja dengan loop - jika kesalahan terjadi di dalam loop, maka goto jauh, jauh lebih bersih daripada alternatif pengaturan dan pengecekan flag dan pernyataan 'break' (yang hanya merupakan gotos yang disamarkan dengan cerdik).
Adam Rosenfield
1
@Smith, Lebih mirip mengemudi tanpa rompi anti peluru.
strager
6
Saya tahu saya sedang necromancing di sini, tetapi menurut saya saran ini agak buruk - panah anti-pola harus dihindari.
KingRadical
16

Saya terkejut tidak ada yang menyarankan alternatif ini, jadi meskipun pertanyaannya sudah ada beberapa saat saya akan menambahkannya: satu cara yang baik untuk mengatasi masalah ini adalah dengan menggunakan variabel untuk melacak keadaan saat ini. Ini adalah teknik yang dapat digunakan baik gotodigunakan untuk sampai pada kode pembersihan atau tidak . Seperti teknik pengkodean apa pun, ini memiliki pro dan kontra, dan tidak akan cocok untuk setiap situasi, tetapi jika Anda memilih gaya, itu layak dipertimbangkan - terutama jika Anda ingin menghindari gototanpa berakhir dengan sangat bertingkat if.

Ide dasarnya adalah, untuk setiap tindakan pembersihan yang mungkin perlu dilakukan, ada variabel yang nilainya dapat kita ketahui apakah pembersihan perlu dilakukan atau tidak.

Saya akan menunjukkan gotoversinya terlebih dahulu, karena lebih dekat dengan kode dalam pertanyaan awal.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Satu keuntungan dari hal ini dibandingkan beberapa teknik lainnya adalah, jika urutan fungsi inisialisasi diubah, pembersihan yang benar masih akan terjadi - misalnya, menggunakan switchmetode yang dijelaskan dalam jawaban lain, jika urutan inisialisasi berubah, maka switchharus diedit dengan sangat hati-hati untuk menghindari upaya membersihkan sesuatu yang sebenarnya tidak diinisialisasi sejak awal.

Sekarang, beberapa orang mungkin berpendapat bahwa metode ini menambahkan banyak variabel tambahan - dan memang dalam kasus ini itu benar - tetapi dalam praktiknya seringkali variabel yang ada sudah melacak, atau dapat dibuat untuk melacak, status yang diperlukan. Misalnya, jika prepare_stuff()sebenarnya adalah panggilan ke malloc(), atau ke open(), variabel yang menahan pointer atau deskriptor file yang dikembalikan dapat digunakan - misalnya:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

Sekarang, jika kami juga melacak status kesalahan dengan variabel, kami dapat menghindari gotosepenuhnya, dan masih membersihkan dengan benar, tanpa memiliki lekukan yang semakin dalam dan semakin dalam semakin banyak inisialisasi yang kami butuhkan:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Sekali lagi, ada kritik potensial untuk ini:

  • Bukankah semua itu "jika" merugikan kinerja? Tidak - karena dalam kasus sukses, Anda tetap harus melakukan semua pemeriksaan (jika tidak, Anda tidak memeriksa semua kasus kesalahan); dan dalam kasus kegagalan, sebagian besar kompiler akan mengoptimalkan urutan if (oksofar)pemeriksaan yang gagal ke satu lompatan ke kode pembersihan (tentu saja GCC melakukannya) - dan dalam kasus apa pun, kasus kesalahan biasanya kurang penting untuk kinerja.
  • Bukankah ini menambahkan variabel lain? Dalam hal ini ya, tetapi seringkali return_valuevariabel dapat digunakan untuk memainkan peran yang oksofardimainkan di sini. Jika Anda menyusun fungsi Anda untuk mengembalikan kesalahan secara konsisten, Anda bahkan dapat menghindari kesalahan kedua ifdalam setiap kasus:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }
    

    Salah satu keuntungan dari pengkodean seperti itu adalah bahwa konsistensi berarti bahwa setiap tempat di mana pemrogram asli lupa untuk memeriksa nilai yang dikembalikan tampak seperti jempol yang sakit, membuatnya lebih mudah untuk menemukan (satu kelas dari) bug.

Jadi - ini (masih) satu gaya lagi yang dapat digunakan untuk menyelesaikan masalah ini. Jika digunakan dengan benar, ini memungkinkan kode yang sangat bersih dan konsisten - dan seperti teknik apa pun, di tangan yang salah dapat menghasilkan kode yang bertele-tele dan membingungkan :-)

psmears
sumber
2
Sepertinya Anda terlambat ke pesta, tapi saya suka jawabannya!
Linus mungkin akan menolak kode blog
Fizz
1
@ user3588161: Jika dia mau, itu hak prerogatifnya - tapi saya tidak yakin, berdasarkan artikel yang Anda tautkan, itulah masalahnya: perhatikan bahwa dalam gaya yang saya gambarkan, (1) kondisional tidak bersarang secara sewenang-wenang, dan (2) tidak ada pernyataan "jika" tambahan dibandingkan dengan yang Anda perlukan (dengan asumsi Anda akan memeriksa semua kode yang dikembalikan).
psmears
Begitu banyak ini, bukan solusi goto yang mengerikan dan bahkan solusi panah-antipattern yang lebih buruk!
The Paramagnetic Croissant
8

Masalah dengan gotokata kunci sebagian besar disalahpahami. Itu tidak benar-benar jahat. Anda hanya perlu menyadari jalur kontrol ekstra yang Anda buat dengan setiap goto. Menjadi sulit untuk bernalar tentang kode Anda dan karenanya validitasnya.

FWIW, jika Anda mencari tutorial developer.apple.com, mereka menggunakan pendekatan goto untuk penanganan error.

Kami tidak menggunakan gotos. Kepentingan yang lebih tinggi diletakkan pada nilai kembali. Penanganan pengecualian dilakukan melalui setjmp/longjmp- apa pun yang Anda bisa.

dirkgently
sumber
8
Meskipun saya sudah pasti menggunakan setjmp / longjmp dalam beberapa kasus di mana itu dipanggil, saya akan menganggapnya bahkan "lebih buruk" daripada goto (yang juga saya gunakan, agak kurang dicadangkan, saat dipanggil). Satu-satunya saat saya menggunakan setjmp / longjmp adalah ketika (1) Target akan menutup semuanya dengan cara yang tidak akan terpengaruh oleh keadaannya saat ini, atau (2) Target akan menginisialisasi ulang semua yang dikontrol di dalamnya blok setjmp / longjmp-guarded dengan cara yang tidak bergantung pada kondisinya saat ini.
supercat
4

Tidak ada yang salah secara moral tentang pernyataan goto, sama seperti ada sesuatu yang salah secara moral dengan petunjuk (void) *.

Semuanya tergantung bagaimana Anda menggunakan alat ini. Dalam kasus (sepele) yang Anda sajikan, pernyataan kasus dapat mencapai logika yang sama, meskipun dengan lebih banyak overhead. Pertanyaan sebenarnya adalah, "apa persyaratan kecepatan saya?"

pergi hanya dengan cepat, terutama jika Anda berhati-hati untuk memastikan bahwa itu dikompilasi ke lompatan pendek. Sempurna untuk aplikasi yang mengutamakan kecepatan. Untuk aplikasi lain, mungkin masuk akal untuk melakukan overhead hit dengan case if / else + untuk pemeliharaan.

Ingat: goto tidak mematikan aplikasi, pengembang membunuh aplikasi.

UPDATE: Berikut contoh kasusnya

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 
webmarc
sumber
1
Bisakah Anda menyajikan alternatif 'kasus'? Juga, saya berpendapat bahwa ini jauh berbeda dari void *, yang diperlukan untuk abstraksi struktur data yang serius di C. Saya tidak berpikir ada orang yang secara serius menentang void *, dan Anda tidak akan menemukan satu basis kode besar tanpa Itu.
Eli Bendersky
Re: void *, itulah maksud saya, tidak ada yang salah secara moral juga. Contoh sakelar / casing di bawah ini. int foo (int bar) {int return_value = 0; int failure_value = 0; if (! do_something (bar)) {failure_value = 1; } lain jika (! init_stuff (bar)) {failure_value = 2; } lain jika (persiapkan_barang (bar)) {{return_value = do_the_thing (bar); cleanup_3 (); } saklar (nilai_gagal) {case 2: cleanup_2 (); istirahat; kasus 1: pembersihan_1 (); istirahat; default: istirahat; }}
webmarc
5
@webmarc, maaf tapi ini mengerikan! Anda baru saja menyimulasikan goto dengan label sepenuhnya - menemukan nilai non-deskriptif Anda sendiri untuk label dan menerapkan goto dengan sakelar / casing. Failure_value = 1 lebih bersih daripada "goto cleanup_something"?
Eli Bendersky
4
Saya merasa seperti Anda menjebak saya di sini ... pertanyaan Anda adalah untuk mendapatkan pendapat, dan apa yang saya inginkan. Namun ketika saya menawarkan jawaban saya, itu mengerikan. :-( Mengenai keluhan Anda tentang nama label, mereka sama deskriptifnya dengan contoh Anda yang lain: cleanup_1, foo, bar. Mengapa Anda menyerang nama label padahal itu tidak sesuai dengan pertanyaannya?
webmarc
1
Saya tidak punya niat untuk "mengatur" dan menimbulkan perasaan negatif apa pun, maaf untuk itu! Rasanya seperti pendekatan baru Anda ditargetkan hanya pada 'penghapusan goto', tanpa menambahkan kejelasan lagi. Seolah-olah Anda telah menerapkan kembali apa yang dilakukan goto, hanya menggunakan lebih banyak kode yang kurang jelas. IMHO ini bukanlah hal yang baik untuk dilakukan - menyingkirkan kebagian hanya demi itu.
Eli Bendersky
2

GOTO berguna. Itu adalah sesuatu yang dapat dilakukan prosesor Anda dan inilah mengapa Anda harus memiliki akses ke sana.

Terkadang Anda ingin menambahkan sedikit sesuatu ke fungsi Anda dan satu goto memungkinkan Anda melakukannya dengan mudah. Dapat menghemat waktu ..

toto
sumber
3
Anda tidak memerlukan akses ke semua hal yang dapat dilakukan prosesor Anda. Sering kali, goto lebih membingungkan daripada alternatifnya.
David Thornley
@DavidThornley: Ya, Anda memang memerlukan akses ke setiap hal yang dapat dilakukan prosesor Anda, jika tidak, Anda akan membuang-buang prosesor. Goto adalah instruksi terbaik dalam pemrograman.
Ron Maimon
1

Secara umum, saya akan menganggap fakta bahwa sepotong kode dapat ditulis dengan paling jelas gotosebagai gejala bahwa aliran program kemungkinan besar lebih rumit daripada yang umumnya diinginkan. Menggabungkan struktur program lain dengan cara yang aneh untuk menghindari penggunaan gotoakan berusaha untuk mengobati gejala, daripada penyakitnya. Contoh khusus Anda mungkin tidak terlalu sulit untuk diterapkan tanpa goto:

  lakukan {
    .. atur thing1 yang membutuhkan pembersihan hanya jika keluar lebih awal
    jika (kesalahan) rusak;
    melakukan
    {
      .. atur thing2 yang perlu dibersihkan jika keluar lebih awal
      jika (kesalahan) rusak;
      // ***** LIHAT TEKS TENTANG BARIS INI
    } sementara (0);
    .. pembersihan hal2;
  } sementara (0);
  .. pembersihan hal1;

tetapi jika pembersihan hanya seharusnya terjadi saat fungsi gagal, gotokasus tersebut dapat ditangani dengan meletakkan returntepat sebelum label target pertama. Kode di atas akan membutuhkan penambahan a returnpada baris yang ditandai dengan *****.

Dalam skenario "pembersihan bahkan dalam kasus normal", saya akan menganggap penggunaan gotolebih jelas daripada do/ while(0)konstruksi, antara lain karena label target sendiri secara praktis meneriakkan "LIHAT ME" jauh lebih banyak daripada breakdan do/ while(0)konstruksi. Untuk kasus "pembersihan hanya jika error", returnpernyataan akhirnya harus berada di tempat yang paling buruk dari sudut pandang keterbacaan (pernyataan kembalian umumnya harus baik di awal fungsi, atau pada apa yang "terlihat seperti" tamat); memiliki returnlabel tepat sebelum target memenuhi kualifikasi itu jauh lebih mudah daripada memiliki label tepat sebelum akhir "lingkaran".

BTW, satu skenario di mana saya terkadang menggunakan gotountuk penanganan kesalahan ada dalam switchpernyataan, ketika kode untuk beberapa kasus berbagi kode kesalahan yang sama. Meskipun kompiler saya seringkali cukup pintar untuk mengenali bahwa banyak kasus diakhiri dengan kode yang sama, saya pikir lebih jelas untuk mengatakan:

 REPARSE_PACKET:
  saklar (paket [0])
  {
    kasus PKT_THIS_OPERATION:
      if (kondisi masalah)
        buka PACKET_ERROR;
      ... menangani THIS_OPERATION
      istirahat;
    kasus PKT_THAT_OPERATION:
      if (kondisi masalah)
        buka PACKET_ERROR;
      ... menangani THAT_OPERATION
      istirahat;
    ...
    kasus PKT_PROCESS_CONDITIONALLY
      jika (panjang_paket <9)
        buka PACKET_ERROR;
      if (kondisi_paket melibatkan paket [4])
      {
        panjang_paket - = 5;
        memmove (paket, paket + 5, panjang_paket);
        buka REPARSE_PACKET;
      }
      lain
      {
        paket [0] = PKT_CONDITION_SKIPPED;
        paket [4] = panjang_paket;
        panjang_paket = 5;
        packet_status = READY_TO_SEND;
      }
      istirahat;
    ...
    default:
    {
     PACKET_ERROR:
      packet_error_count ++;
      panjang_paket = 4;
      paket [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      istirahat;
    }
  }   

Meskipun seseorang dapat mengganti gotopernyataan dengan {handle_error(); break;}, dan meskipun seseorang dapat menggunakan do/ while(0)loop bersama continueuntuk memproses paket eksekusi bersyarat yang dibungkus, saya tidak berpikir itu lebih jelas daripada menggunakan file goto. Lebih lanjut, meskipun dimungkinkan untuk menyalin kode dari PACKET_ERRORmana saja yang goto PACKET_ERRORdigunakan, dan sementara kompiler mungkin menulis kode duplikat sekali dan mengganti sebagian besar kejadian dengan lompatan ke salinan bersama itu, penggunaan the gotomembuatnya lebih mudah untuk melihat tempat yang mengatur paket sedikit berbeda (misalnya jika instruksi "mengeksekusi secara kondisional" memutuskan untuk tidak mengeksekusi).

supercat
sumber
1

Saya pribadi adalah pengikut "The Power of Ten - 10 Rules for Menulis Safety Critical Code" .

Saya akan menyertakan cuplikan kecil dari teks tersebut yang menggambarkan apa yang saya yakini sebagai ide bagus tentang goto.


Aturan: Batasi semua kode ke konstruksi aliran kontrol yang sangat sederhana - jangan gunakan pernyataan goto, konstruksi setjmp atau longjmp, dan rekursi langsung atau tidak langsung.

Rasional: Alur kontrol yang lebih sederhana diterjemahkan menjadi kemampuan yang lebih kuat untuk verifikasi dan sering kali menghasilkan kejelasan kode yang lebih baik. Pengusiran rekursi mungkin merupakan kejutan terbesar di sini. Namun, tanpa rekursi, kita dijamin memiliki grafik panggilan fungsi asiklik, yang dapat dieksploitasi oleh penganalisis kode, dan dapat secara langsung membantu membuktikan bahwa semua eksekusi yang seharusnya dibatasi sebenarnya dibatasi. (Perhatikan bahwa aturan ini tidak mengharuskan semua fungsi memiliki satu titik pengembalian - meskipun ini sering juga menyederhanakan aliran kontrol. Namun, ada cukup banyak kasus, di mana pengembalian kesalahan awal adalah solusi yang lebih sederhana.)


Mengusir penggunaan goto tampaknya buruk, tetapi:

Jika aturannya tampak Draconian pada awalnya, ingatlah bahwa aturan tersebut dimaksudkan untuk memungkinkan pengecekan kode yang secara harfiah hidup Anda mungkin bergantung pada kebenarannya: kode yang digunakan untuk mengendalikan pesawat yang Anda tumpangi, pembangkit listrik tenaga nuklir beberapa mil dari tempat tinggal Anda, atau pesawat ruang angkasa yang membawa astronot ke orbit. Aturan berlaku seperti sabuk pengaman di mobil Anda: awalnya mungkin sedikit tidak nyaman, tetapi setelah beberapa saat penggunaannya menjadi kebiasaan dan tidak menggunakannya menjadi tidak terbayangkan.

Trevor Boyd Smith
sumber
22
Masalahnya adalah bahwa cara yang biasa untuk menghapus secara total gotoadalah dengan menggunakan beberapa set boolean "pintar" dalam ifs atau loop yang sangat bertingkat. Itu tidak membantu. Mungkin alat Anda akan bekerja lebih baik, tetapi Anda tidak akan melakukannya dan Anda lebih penting.
Donal Fellows
1

Saya setuju bahwa pembersihan goto dalam urutan terbalik yang diberikan dalam pertanyaan adalah cara terbersih untuk membersihkan berbagai hal di sebagian besar fungsi. Tetapi saya juga ingin menunjukkan bahwa terkadang, Anda tetap ingin fungsi Anda bersih. Dalam kasus ini saya menggunakan varian berikut jika idiom if (0) {label:} untuk menuju ke titik yang benar dari proses pembersihan:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }
pengguna1251840
sumber
0

Menurut saya yang cleanup_3harus melakukan pembersihan, lalu panggil cleanup_2. Demikian pula, cleanup_2harus lakukan pembersihan itu, lalu panggil pembersihan_1. Tampaknya kapan pun Anda melakukannya cleanup_[n], itu cleanup_[n-1]diperlukan, sehingga harus menjadi tanggung jawab metode (sehingga, misalnya, cleanup_3tidak pernah dapat dipanggil tanpa menelepon cleanup_2dan mungkin menyebabkan kebocoran.)

Mengingat pendekatan itu, alih-alih gotos, Anda cukup memanggil rutinitas pembersihan, lalu kembali.

The gotoPendekatan ini tidak salah atau buruk , meskipun, itu hanya perlu dicatat bahwa itu belum tentu "terbersih" pendekatan (IMHO).

Jika Anda mencari kinerja yang optimal, maka saya kira gotosolusinya adalah yang terbaik. Saya hanya berharap itu relevan, namun, dalam beberapa aplikasi tertentu, kinerja kritis, (misalnya, driver perangkat, perangkat tertanam, dll). Jika tidak, ini adalah pengoptimalan mikro yang memiliki prioritas lebih rendah daripada kejelasan kode.

Ryan Emerle
sumber
4
Itu tidak akan memotong - pembersihan khusus untuk sumber daya, yang dialokasikan dalam urutan ini hanya dalam rutinitas ini. Di tempat lain, mereka tidak berhubungan, jadi menelepon satu sama lain tidak masuk akal.
Eli Bendersky
0

Saya pikir pertanyaan di sini salah sehubungan dengan kode yang diberikan.

Mempertimbangkan:

  1. do_something (), init_stuff () dan prepared_stuff () tampaknya mengetahui apakah mereka gagal, karena mereka mengembalikan salah atau nil dalam kasus itu.
  2. Tanggung jawab untuk menyiapkan status tampaknya menjadi tanggung jawab fungsi tersebut, karena tidak ada status yang disiapkan secara langsung di foo ().

Oleh karena itu: do_something (), init_stuff () dan prepared_stuff () harus melakukan pembersihannya sendiri . Memiliki fungsi cleanup_1 () terpisah yang membersihkan setelah do_something () merusak filosofi enkapsulasi. Itu desain yang buruk.

Jika mereka melakukan pembersihan sendiri, foo () menjadi sangat sederhana.

Di samping itu. Jika foo () benar-benar membuat statusnya sendiri yang perlu dihancurkan, maka goto akan sesuai.

Simon Woodside
sumber
0

Inilah yang saya sukai:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}
Mikko Korkalo
sumber
0

Diskusi lama, bagaimanapun .... bagaimana dengan menggunakan "pola anti panah" dan kemudian merangkum setiap tingkat bersarang dalam fungsi sebaris statis? Kode terlihat bersih, optimal (saat pengoptimalan diaktifkan), dan tidak ada goto yang digunakan. Singkatnya, bagi dan taklukkan. Di bawah sebuah contoh:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

Dalam hal ruang, kami membuat tiga kali variabel dalam tumpukan, yang tidak baik, tetapi ini menghilang saat mengompilasi dengan -O2 menghapus variabel dari tumpukan dan menggunakan register dalam contoh sederhana ini. Apa yang saya dapatkan dari blok di atas gcc -S -O2 test.cadalah di bawah ini:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols
Alejandro Visiedo García
sumber
-1

Saya lebih suka menggunakan teknik yang dijelaskan dalam contoh berikut ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

sumber: http://blog.staila.com/?p=114

Nitin Kunal
sumber
2
kode flaggy dan panah anti-pola (keduanya ditampilkan dalam contoh Anda) adalah dua hal yang memperumit kode. Tidak ada pembenaran di luar "goto is evil" untuk menggunakannya.
KingRadical
-1

Kami menggunakan Daynix CStepsperpustakaan sebagai solusi lain untuk " masalah goto " dalam fungsi init.
Lihat disini dan disini .

Komputasi Daynix LTD
sumber
3
penyalahgunaan makro (seperti CSteps) jauh lebih buruk daripada penggunaan yang bijaksanagoto
KingRadical