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 goto
sini 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 goto
di C? Apakah Anda lebih suka metode lain, yang menghasilkan kode yang lebih berbelit-belit dan / atau kurang efisien, tetapi hindari goto
?
sumber
on error goto error
sistem penanganan kesalahan :)Jawaban:
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
goto
secara umum merupakan ide yang buruk, ini dapat berguna untuk penanganan kesalahan jika dilakukan dengan cara yang sederhana dan seragam. Dalam situasi ini, meskipun itu agoto
, itu digunakan dengan cara yang didefinisikan dengan baik dan lebih atau kurang terstruktur.sumber
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.
sumber
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
goto
digunakan 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 menghindarigoto
tanpa berakhir dengan sangat bertingkatif
.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
goto
versinya 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
switch
metode yang dijelaskan dalam jawaban lain, jika urutan inisialisasi berubah, makaswitch
harus 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 kemalloc()
, atau keopen()
, 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
goto
sepenuhnya, 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:
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_value
variabel dapat digunakan untuk memainkan peran yangoksofar
dimainkan di sini. Jika Anda menyusun fungsi Anda untuk mengembalikan kesalahan secara konsisten, Anda bahkan dapat menghindari kesalahan keduaif
dalam 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 :-)
sumber
Masalah dengan
goto
kata 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.sumber
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 ; } }
sumber
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 ..
sumber
Secara umum, saya akan menganggap fakta bahwa sepotong kode dapat ditulis dengan paling jelas
goto
sebagai gejala bahwa aliran program kemungkinan besar lebih rumit daripada yang umumnya diinginkan. Menggabungkan struktur program lain dengan cara yang aneh untuk menghindari penggunaangoto
akan berusaha untuk mengobati gejala, daripada penyakitnya. Contoh khusus Anda mungkin tidak terlalu sulit untuk diterapkan tanpagoto
:tetapi jika pembersihan hanya seharusnya terjadi saat fungsi gagal,
goto
kasus tersebut dapat ditangani dengan meletakkanreturn
tepat sebelum label target pertama. Kode di atas akan membutuhkan penambahan areturn
pada baris yang ditandai dengan*****
.Dalam skenario "pembersihan bahkan dalam kasus normal", saya akan menganggap penggunaan
goto
lebih jelas daripadado
/while(0)
konstruksi, antara lain karena label target sendiri secara praktis meneriakkan "LIHAT ME" jauh lebih banyak daripadabreak
dando
/while(0)
konstruksi. Untuk kasus "pembersihan hanya jika error",return
pernyataan 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); memilikireturn
label tepat sebelum target memenuhi kualifikasi itu jauh lebih mudah daripada memiliki label tepat sebelum akhir "lingkaran".BTW, satu skenario di mana saya terkadang menggunakan
goto
untuk penanganan kesalahan ada dalamswitch
pernyataan, 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:Meskipun seseorang dapat mengganti
goto
pernyataan dengan{handle_error(); break;}
, dan meskipun seseorang dapat menggunakando
/while(0)
loop bersamacontinue
untuk memproses paket eksekusi bersyarat yang dibungkus, saya tidak berpikir itu lebih jelas daripada menggunakan filegoto
. Lebih lanjut, meskipun dimungkinkan untuk menyalin kode dariPACKET_ERROR
mana saja yanggoto PACKET_ERROR
digunakan, dan sementara kompiler mungkin menulis kode duplikat sekali dan mengganti sebagian besar kejadian dengan lompatan ke salinan bersama itu, penggunaan thegoto
membuatnya lebih mudah untuk melihat tempat yang mengatur paket sedikit berbeda (misalnya jika instruksi "mengeksekusi secara kondisional" memutuskan untuk tidak mengeksekusi).sumber
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.
sumber
goto
adalah 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.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 ; }
sumber
Menurut saya yang
cleanup_3
harus melakukan pembersihan, lalu panggilcleanup_2
. Demikian pula,cleanup_2
harus lakukan pembersihan itu, lalu panggil pembersihan_1. Tampaknya kapan pun Anda melakukannyacleanup_[n]
, itucleanup_[n-1]
diperlukan, sehingga harus menjadi tanggung jawab metode (sehingga, misalnya,cleanup_3
tidak pernah dapat dipanggil tanpa meneleponcleanup_2
dan mungkin menyebabkan kebocoran.)Mengingat pendekatan itu, alih-alih gotos, Anda cukup memanggil rutinitas pembersihan, lalu kembali.
The
goto
Pendekatan 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
goto
solusinya 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.sumber
Saya pikir pertanyaan di sini salah sehubungan dengan kode yang diberikan.
Mempertimbangkan:
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.
sumber
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; }
sumber
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.c
adalah 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
sumber
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
sumber
Kami menggunakan
Daynix CSteps
perpustakaan sebagai solusi lain untuk " masalah goto " dalam fungsi init.Lihat disini dan disini .
sumber
goto