Mengapa tidak ada konstruksi 'akhirnya' di C ++?

57

Penanganan pengecualian di C ++ terbatas untuk mencoba / melempar / menangkap. Tidak seperti Object Pascal, Java, C # dan Python, bahkan dalam C ++ 11, finallykonstruksinya belum diimplementasikan.

Saya telah melihat banyak sekali literatur C ++ yang membahas "pengecualian kode aman". Lippman menulis bahwa pengecualian kode aman adalah topik yang penting namun canggih, sulit, di luar cakupan Primer-nya - yang tampaknya menyiratkan bahwa kode aman tidak mendasar bagi C ++. Herb Sutter menyediakan 10 bab untuk topik ini dalam C ++ Luar Biasa!

Namun bagi saya tampaknya banyak masalah yang dihadapi ketika mencoba untuk menulis "pengecualian kode aman" dapat diselesaikan dengan baik jika finallykonstruksinya diimplementasikan, yang memungkinkan programmer untuk memastikan bahwa bahkan jika ada pengecualian, program dapat dipulihkan. ke keadaan aman, stabil, bebas kebocoran, dekat dengan titik alokasi sumber daya dan kode berpotensi bermasalah. Sebagai programmer Delphi dan C # yang sangat berpengalaman saya menggunakan try .. akhirnya memblokir kode saya secara ekstensif, seperti halnya kebanyakan programmer dalam bahasa ini.

Mempertimbangkan semua 'lonceng dan peluit' yang diimplementasikan dalam C ++ 11, saya terkejut menemukan bahwa 'akhirnya' masih belum ada.

Jadi, mengapa finallykonstruksinya tidak pernah diimplementasikan dalam C ++? Ini benar-benar bukan konsep yang sangat sulit atau lanjutan untuk dipahami dan berjalan jauh ke arah membantu programmer untuk menulis 'pengecualian kode aman'.

Vektor
sumber
25
Kenapa tidak akhirnya? Karena Anda melepaskan hal-hal di destruktor yang menyala secara otomatis ketika objek (atau penunjuk pintar) meninggalkan ruang lingkup. Destructors lebih unggul daripada {} karena memisahkan alur kerja dari logika pembersihan. Sama seperti Anda tidak ingin panggilan gratis () mengacaukan alur kerja Anda dalam bahasa sampah yang dikumpulkan.
mike30
2
Lihat juga Apakah para pengembang Java dengan sadar meninggalkan RAII?
BlueRaja - Danny Pflughoeft
8
Mengajukan pertanyaan, "Mengapa tidak ada finallydalam C ++, dan teknik apa untuk penanganan pengecualian yang digunakan sebagai gantinya?" valid dan sesuai topik untuk situs ini. Jawaban yang ada mencakup ini dengan baik, saya pikir. Mengubahnya menjadi diskusi tentang "Apakah alasan desainer C ++ untuk tidak termasuk finallyberharga?" dan "Harus finallyditambahkan ke C ++?" dan melanjutkan diskusi lintas komentar pada pertanyaan dan setiap jawaban tidak cocok dengan model situs tanya jawab ini.
Josh Kelley
2
Jika akhirnya, Anda sudah memiliki pemisahan kekhawatiran: blok kode utama ada di sini, dan masalah pembersihan diatasi di sini.
Kaz
2
@ Ka. Perbedaannya adalah implisit vs pembersihan eksplisit. Sebuah destructor memberi Anda pembersihan otomatis yang mirip dengan cara primitif lama yang polos dibersihkan saat tumpukan itu keluar. Anda tidak perlu membuat panggilan pembersihan eksplisit dan dapat fokus pada logika inti Anda. Bayangkan betapa berbelit-belitnya jika Anda harus membersihkan tumpukan yang dialokasikan primitif dalam coba / akhirnya. Pembersihan implisit lebih unggul. Perbandingan sintaksis kelas dengan fungsi anonim tidak relevan. Meskipun dengan melewatkan fungsi kelas satu ke fungsi yang melepaskan pegangan bisa memusatkan pembersihan manual.
mike30

Jawaban:

57

Seperti beberapa komentar tambahan pada jawaban @ Nemanja (yang, karena mengutip Stroustrup, benar-benar sama baiknya dengan jawaban yang Anda dapatkan):

Ini benar-benar hanya masalah memahami filosofi dan idiom C ++. Ambil contoh operasi yang membuka koneksi database pada kelas persisten dan harus memastikan bahwa itu menutup koneksi jika pengecualian dilemparkan. Ini adalah masalah keamanan pengecualian dan berlaku untuk bahasa apa pun dengan pengecualian (C ++, C #, Delphi ...).

Dalam bahasa yang menggunakan try/ finally, kode tersebut mungkin terlihat seperti ini:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Sederhana dan mudah. Namun, ada beberapa kelemahan:

  • Jika bahasa tidak memiliki destruktor deterministik, saya selalu harus menulis finallyblok, kalau tidak saya membocorkan sumber daya.
  • Jika DoRiskyOperationlebih dari satu pemanggilan metode tunggal - jika saya memiliki beberapa pemrosesan yang harus dilakukan di tryblok - maka Closeoperasi dapat berakhir menjadi agak jauh dari Openoperasi. Saya tidak bisa menulis pembersihan di sebelah akuisisi saya.
  • Jika saya memiliki beberapa sumber daya yang perlu diperoleh kemudian dibebaskan dengan cara pengecualian-aman, saya bisa berakhir dengan beberapa lapisan dalam try/ finallyblok.

Pendekatan C ++ akan terlihat seperti ini:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

Ini sepenuhnya menyelesaikan semua kelemahan dari finallypendekatan. Ini memiliki beberapa kelemahan sendiri, tetapi mereka relatif kecil:

  • Ada peluang bagus Anda harus menulis ScopedDatabaseConnectionsendiri kelas itu. Namun, ini adalah implementasi yang sangat sederhana - hanya 4 atau 5 baris kode.
  • Ini melibatkan pembuatan variabel lokal ekstra - yang Anda tampaknya bukan penggemar, berdasarkan komentar Anda tentang "terus-menerus membuat dan menghancurkan kelas untuk bergantung pada destruktor mereka untuk membersihkan kekacauan Anda sangat buruk" - tetapi kompiler yang baik akan mengoptimalkan keluar dari pekerjaan ekstra yang melibatkan variabel lokal tambahan. Desain C ++ yang baik sangat bergantung pada optimasi semacam ini.

Secara pribadi, mengingat kelebihan dan kekurangan ini, saya menemukan teknik RAII yang lebih disukai finally. Jarak tempuh Anda mungkin beragam.

Akhirnya, karena RAII adalah idiom mapan dalam C ++, dan untuk meringankan pengembang dari beberapa beban menulis banyak Scoped...kelas, ada perpustakaan seperti ScopeGuard dan Boost.ScopeExit yang memfasilitasi semacam pembersihan deterministik ini.

Josh Kelley
sumber
8
C # memiliki usingpernyataan, yang secara otomatis membersihkan objek yang mengimplementasikan IDisposableantarmuka. Jadi, meskipun mungkin salah, sangat mudah melakukannya dengan benar.
Robert Harvey
18
Harus menulis kelas yang sama sekali baru untuk mengurus pembalikan perubahan keadaan sementara, menggunakan idiom desain yang diimplementasikan oleh kompiler dengan sebuah try/finallykonstruk karena kompiler tidak memaparkan try/finallykonstruk dan satu-satunya cara untuk mengaksesnya adalah melalui kelas berbasis idiom desain, bukan merupakan "keuntungan;" itu adalah definisi inversi abstraksi.
Mason Wheeler
15
@MasonWheeler - Umm, saya tidak mengatakan bahwa harus menulis kelas baru adalah keuntungan. Saya bilang itu kerugian. Namun, saya lebih memilih RAII daripada harus menggunakan finally. Seperti yang saya katakan, jarak tempuh Anda mungkin berbeda.
Josh Kelley
7
@JoshKelley: 'Desain C ++ yang bagus sangat bergantung pada optimisasi semacam ini.' Menulis sekumpulan kode asing dan kemudian mengandalkan optimisasi kompiler adalah Desain Bagus ?! IMO itu kebalikan dari desain yang baik. Di antara dasar-dasar desain yang baik adalah kode yang ringkas dan mudah dibaca. Kurang untuk debug, kurang pemeliharaan, dll dll. Anda TIDAK boleh menulis sekumpulan kode dan kemudian bergantung pada kompiler untuk membuat semuanya hilang - IMO yang tidak masuk akal sama sekali!
Vektor
14
@ Mikey: Jadi menduplikasi kode pembersihan (atau fakta bahwa pembersihan harus terjadi) di seluruh basis kode adalah "ringkas" dan "mudah dibaca"? Dengan RAII, Anda menulis kode seperti itu satu kali, dan secara otomatis diterapkan di mana-mana.
Mankarse
55

Dari Mengapa C ++ tidak menyediakan konstruksi "akhirnya"? di FAQ Gaya dan Teknik C ++ Bjarne Stroustrup :

Karena C ++ mendukung alternatif yang hampir selalu lebih baik: Teknik "akuisisi sumber daya adalah inisialisasi" (TC ++ PL3 bagian 14.4). Ide dasarnya adalah untuk mewakili sumber daya oleh objek lokal, sehingga destruktor objek lokal akan melepaskan sumber daya. Dengan begitu, programmer tidak bisa melupakan untuk merilis resource.

Nemanja Trifunovic
sumber
5
Tapi tidak ada apa-apa tentang teknik yang khusus untuk C ++, kan? Anda dapat melakukan RAII dalam bahasa apa pun dengan objek, konstruktor, dan destruktor. Ini adalah teknik yang hebat, tetapi RAII yang hanya ada tidak menyiratkan bahwa sebuah finallykonstruksi selalu tidak berguna selamanya, terlepas dari apa yang dikatakan Strousup. Fakta bahwa menulis "pengecualian kode aman" adalah masalah besar dalam C ++ adalah buktinya. Heck, C # memiliki kedua destructor dan finally, dan keduanya digunakan.
Tacroy
28
@ Trac: C ++ adalah salah satu dari sedikit bahasa utama yang memiliki destruktor deterministik . C # "destructors" tidak berguna untuk tujuan ini, dan Anda perlu menulis "menggunakan" blok secara manual untuk mendapatkan RAII.
Nemanja Trifunovic
15
@ Mikey Anda memiliki jawaban "Mengapa C ++ tidak menyediakan konstruksi" akhirnya "?" langsung dari Stroustrup sendiri di sana. Apa lagi yang bisa Anda minta? Itu adalah mengapa.
5
@Mikey Jika Anda khawatir tentang kode Anda berperilaku baik, sumber daya tertentu tidak bocor, ketika pengecualian dilemparkan pada itu, Anda yang mengkhawatirkan tentang keselamatan pengecualian / mencoba untuk menulis pengecualian kode yang aman. Anda tidak menyebutnya demikian, dan karena ada berbagai alat yang tersedia, Anda menerapkannya secara berbeda. Tapi itulah yang dibicarakan orang C ++ ketika mereka membahas keamanan pengecualian.
19
@ Ka: Saya hanya perlu ingat untuk melakukan pembersihan di destructor sekali, dan sejak saat itu saya hanya menggunakan objek. Saya harus ingat untuk melakukan pembersihan di blok akhirnya setiap kali saya menggunakan operasi yang mengalokasikan.
deworde
19

Alasan yang tidak dimiliki C ++ finallyadalah karena tidak diperlukan dalam C ++. finallydigunakan untuk mengeksekusi beberapa kode terlepas dari apakah pengecualian telah terjadi atau tidak, yang hampir selalu merupakan semacam kode pembersihan. Dalam C ++, kode pembersihan ini harus dalam destruktor dari kelas yang relevan dan destruktor akan selalu dipanggil, seperti halnya sebuah finallyblok. Ungkapan menggunakan destructor untuk pembersihan Anda disebut RAII .

Di dalam komunitas C ++ mungkin ada lebih banyak pembicaraan tentang kode 'pengecualian aman', tetapi hampir sama pentingnya dalam bahasa lain yang memiliki pengecualian. Inti dari kode 'pengecualian aman' adalah bahwa Anda memikirkan dalam keadaan apa kode Anda dibiarkan jika pengecualian terjadi di salah satu fungsi / metode yang Anda panggil.
Dalam C ++, kode 'exception safe' sedikit lebih penting, karena C ++ tidak memiliki pengumpulan sampah otomatis yang menangani objek yang ditinggalkan yatim karena pengecualian.

Alasan keamanan pengecualian lebih banyak dibahas di komunitas C ++ mungkin juga berasal dari kenyataan bahwa di C ++ Anda harus lebih waspada terhadap apa yang bisa salah, karena ada lebih sedikit jaring pengaman default dalam bahasa tersebut.

Bart van Ingen Schenau
sumber
2
Catatan: Tolong jangan berpendapat bahwa C ++ memiliki destruktor deterministik. Objek Pascal / Delphi juga memiliki destruktor deterministik namun juga mendukung 'akhirnya', untuk alasan yang sangat baik saya jelaskan dalam komentar pertama saya di bawah ini.
Vektor
13
@ Mikey: Mengingat bahwa tidak pernah ada proposal untuk menambah finallystandar C ++, saya pikir aman untuk menyimpulkan bahwa komunitas C ++ tidak menganggap the absence of finallymasalah. Sebagian besar bahasa memiliki finallykekurangan deterministik konsisten yang dimiliki oleh C ++. Saya melihat bahwa Delphi memiliki keduanya, tetapi saya tidak tahu sejarahnya dengan cukup baik untuk mengetahui yang mana yang ada terlebih dahulu.
Bart van Ingen Schenau
3
Dephi tidak mendukung objek berbasis stack - hanya berbasis heap, dan referensi objek pada stack. Karena itu 'akhirnya' diperlukan untuk secara eksplisit memanggil destruktor dll bila perlu.
Vektor
2
Ada banyak kesalahan dalam C ++ yang bisa dibilang tidak diperlukan, jadi ini bukan jawaban yang tepat.
Kaz
15
Dalam lebih dari dua dekade saya telah menggunakan bahasa tersebut, dan bekerja dengan orang lain yang menggunakan bahasa tersebut, saya belum pernah menemukan seorang programmer C ++ yang bekerja yang mengatakan "Saya benar-benar berharap bahasa tersebut memiliki finally". Saya tidak dapat mengingat tugas apa pun yang akan menjadi lebih mudah, seandainya saya memiliki akses ke sana.
Gort the Robot
12

Yang lain telah membahas RAII sebagai solusinya. Ini solusi yang sangat bagus. Tetapi itu tidak benar-benar membahas mengapa mereka tidak menambahkan finallyjuga karena itu adalah hal yang diinginkan secara luas. Jawabannya lebih mendasar pada desain dan pengembangan C ++: selama pengembangan C ++ mereka yang terlibat telah sangat menentang pengenalan fitur desain yang dapat dicapai dengan menggunakan fitur lain tanpa banyak keributan dan terutama di mana ini membutuhkan pengenalan kata kunci baru yang dapat membuat kode lama tidak kompatibel. Karena RAII menyediakan alternatif yang sangat fungsional finallydan Anda sebenarnya dapat memutar sendiri finallydi C ++ 11, ada sedikit panggilan untuk itu.

Yang perlu Anda lakukan adalah membuat kelas Finallyyang memanggil fungsi yang diteruskan ke konstruktor di destruktor itu. Maka Anda dapat melakukan ini:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

Kebanyakan programmer C ++ asli, secara umum, lebih suka objek RAII yang dirancang dengan rapi.

Jack Aidley
sumber
3
Anda kehilangan tangkapan referensi di lambda Anda. Seharusnya Finally atEnd([&] () { database.close(); });juga, saya membayangkan yang berikut ini lebih baik: { Finally atEnd(...); try {...} catch(e) {...} }(Saya mengangkat finalizer dari blok uji coba sehingga dieksekusi setelah blok tangkapan.)
Thomas Eding
2

Anda dapat menggunakan pola "perangkap" - bahkan jika Anda tidak ingin menggunakan blok coba / tangkap.

Letakkan objek sederhana dalam cakupan yang diperlukan. Dalam destruktor objek ini masukkan logika "akhirnya" Anda. Tidak peduli apa, ketika tumpukan dibatalkan, destruktor objek akan dipanggil dan Anda akan mendapatkan permen Anda.

Arie R
sumber
1
Ini tidak menjawab pertanyaan, dan hanya membuktikan bahwa akhirnya bukan ide yang buruk ...
Vector
2

Nah, Anda bisa mengurutkan roll Anda sendiri finally, menggunakan Lambdas, yang akan mendapatkan yang berikut untuk dikompilasi dengan baik (menggunakan contoh tanpa RAII tentu saja, bukan potongan kode terbaik):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Lihat artikel ini .

einpoklum - mengembalikan Monica
sumber
-2

Saya tidak yakin saya setuju dengan pernyataan di sini bahwa RAII adalah superset dari finally. Tumit achilles dari RAII sederhana: pengecualian. RAII diimplementasikan dengan destruktor, dan selalu salah dalam C ++ untuk membuang destruktor. Itu berarti bahwa Anda tidak dapat menggunakan RAII ketika Anda perlu membuang kode pembersihan Anda. Jika finallyditerapkan, di sisi lain, tidak ada alasan untuk percaya bahwa itu tidak sah untuk dilempar dari finallyblok.

Pertimbangkan jalur kode seperti ini:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Jika kita punya finallykita bisa menulis:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Tapi tidak ada cara, yang bisa saya temukan, untuk mendapatkan perilaku yang setara menggunakan RAII.

Jika seseorang tahu bagaimana melakukan ini di C ++, saya sangat tertarik dengan jawabannya. Saya bahkan akan senang dengan sesuatu yang mengandalkan, misalnya, menegakkan bahwa semua pengecualian diwarisi dari satu kelas dengan beberapa kemampuan khusus atau sesuatu.

Ilmuwan gila
sumber
1
Dalam contoh kedua Anda, jika complex_cleanupbisa melempar, maka Anda dapat memiliki kasus di mana dua pengecualian tanpa tertangkap sedang terbang sekaligus, sama seperti yang Anda lakukan dengan RAII / destruktor, dan C ++ menolak untuk mengizinkan ini. Jika Anda ingin pengecualian asli dilihat, maka complex_cleanupharus mencegah pengecualian, seperti halnya dengan RAII / destruktor. Jika Anda ingin complex_cleanuppengecualian terlihat, maka saya pikir Anda dapat menggunakan blok coba / tangkap bersarang - meskipun ini bersinggungan dan sulit untuk dimasukkan ke dalam komentar, jadi itu layak untuk pertanyaan terpisah.
Josh Kelley
Saya ingin menggunakan RAII untuk mendapatkan perilaku yang identik sebagai contoh pertama, lebih aman. Lemparan di finallyblok putatif jelas akan bekerja sama dengan lemparan di catchblok pengecualian dalam penerbangan WRT - tidak menelepon std::terminate. Pertanyaannya adalah "mengapa tidak ada finallydi C ++?" dan semua jawaban mengatakan "Anda tidak membutuhkannya ... RAII FTW!" Maksud saya adalah ya, RAII baik-baik saja untuk kasus-kasus sederhana seperti manajemen memori, tetapi sampai masalah pengecualian diselesaikan, memerlukan terlalu banyak pemikiran / overhead / perhatian / desain ulang untuk menjadi solusi tujuan umum.
MadScientist
3
Saya mengerti maksud Anda - ada beberapa masalah yang sah dengan destruktor yang mungkin melempar - tetapi itu jarang terjadi. Mengatakan bahwa pengecualian RAII + memiliki masalah yang belum terselesaikan atau bahwa RAII bukan solusi tujuan umum sama sekali tidak cocok dengan pengalaman sebagian besar pengembang C ++.
Josh Kelley
1
Jika Anda mendapati diri Anda perlu untuk meningkatkan pengecualian pada destruktor, Anda melakukan sesuatu yang salah - mungkin menggunakan pointer di tempat lain ketika mereka tidak diperlukan.
Vektor
1
Ini terlalu terlibat untuk komentar. Posting pertanyaan tentang hal itu: Bagaimana Anda menangani skenario ini dalam C ++ menggunakan model RAII ... sepertinya tidak berfungsi ... Sekali lagi, Anda harus mengarahkan komentar Anda : ketik @ dan nama anggota yang Anda bicarakan untuk di awal komentar Anda. Ketika komentar ada di pos Anda sendiri, Anda akan diberitahu semuanya, tetapi yang lain tidak, kecuali jika Anda mengarahkan komentar kepada mereka.
Vektor