Apa perbedaan konseptual antara akhirnya dan destruktor?

12

Pertama, saya sangat menyadari mengapa tidak ada konstruksi 'akhirnya' dalam C ++? tetapi diskusi komentar yang panjang tentang pertanyaan lain tampaknya membutuhkan pertanyaan terpisah.

Terlepas dari masalah bahwa finallydalam C # dan Java pada dasarnya hanya dapat ada satu kali (== 1) per cakupan dan satu cakupan dapat memiliki beberapa (== n) C + + destructors, saya pikir mereka pada dasarnya adalah hal yang sama. (Dengan beberapa perbedaan teknis.)

Namun, pengguna lain berpendapat :

... Saya mencoba untuk mengatakan bahwa dtor pada dasarnya adalah alat untuk (Release sematics) dan akhirnya secara inheren merupakan alat untuk (Commant semantic). Jika Anda tidak melihat alasannya: pertimbangkan mengapa melegalkan satu sama lain di blok akhirnya sah, dan mengapa hal yang sama tidak untuk destruktor. (Dalam beberapa hal, ini data vs kontrol. Destructor adalah untuk melepaskan data, akhirnya untuk melepaskan kontrol. Mereka berbeda; sangat disayangkan bahwa C ++ mengikat mereka bersama-sama.)

Bisakah seseorang membersihkan ini?

Martin Ba
sumber

Jawaban:

6
  • Transaksi ( try)
  • Keluaran / Respons Kesalahan ( catch)
  • Kesalahan Eksternal ( throw)
  • Kesalahan Programmer ( assert)
  • Kembalikan (hal terdekat mungkin adalah pelindung ruang lingkup dalam bahasa yang mendukungnya secara asli)
  • Melepaskan Sumber Daya (destruktor)
  • Aliran Kontrol Independen Lain-Lain Transaksi ( finally)

Tidak dapat memberikan deskripsi yang lebih baik finallydaripada aliran kontrol independen transaksi lainnya. Itu tidak selalu memetakan begitu langsung ke konsep tingkat tinggi dalam konteks transaksi dan pola pikir pemulihan kesalahan, terutama dalam bahasa teoritis yang memiliki baik destruktor dan finally.

Apa yang paling inheren kurang bagi saya adalah fitur bahasa yang secara langsung mewakili konsep efek samping eksternal. Penjaga ruang lingkup dalam bahasa seperti D adalah hal terdekat yang dapat saya pikirkan yang hampir mewakili konsep itu. Dari sudut pandang aliran kontrol, kemunduran dalam lingkup fungsi tertentu perlu membedakan jalur yang luar biasa dari yang biasa, sementara secara bersamaan mengotomatiskan kemunduran secara implisit dari setiap efek samping yang disebabkan oleh fungsi jika transaksi gagal, tetapi tidak ketika transaksi berhasil . Itu cukup mudah untuk dilakukan dengan destruktor jika kita, katakan, atur boolean ke nilai seperti succeededtrue pada akhir blok percobaan kami untuk mencegah logika rollback pada destruktor. Tapi ini cara yang agak bundar untuk melakukan ini.

Sementara itu mungkin tampak seperti itu tidak akan menghemat banyak, pembalikan efek samping adalah salah satu hal yang paling sulit untuk diperbaiki (mis: apa yang membuatnya sangat sulit untuk menulis sebuah wadah generik yang aman-pengecualian).


sumber
4

Dalam cara mereka - dengan cara yang sama bahwa Ferrari dan transit keduanya dapat digunakan untuk menggigit toko untuk satu liter susu meskipun mereka dirancang untuk kegunaan yang berbeda.

Anda bisa menempatkan coba / akhirnya membangun di setiap lingkup dan membersihkan semua variabel yang didefinisikan ruang lingkup di blok akhirnya untuk meniru sebuah destruktor C ++. Ini, secara konseptual, apa yang dilakukan C ++ - kompiler secara otomatis memanggil destruktor ketika sebuah variabel keluar dari lingkup (yaitu di akhir blok lingkup). Anda harus mengatur percobaan Anda / akhirnya jadi percobaan adalah hal pertama dan akhirnya hal terakhir dalam setiap ruang lingkup. Anda juga harus menetapkan standar untuk setiap objek agar memiliki metode yang diberi nama khusus yang digunakannya untuk membersihkan statusnya yang akan Anda panggil di blok terakhir, meskipun saya rasa Anda dapat meninggalkan manajemen memori normal yang disediakan bahasa Anda untuk bersihkan objek yang sekarang dikosongkan saat suka.

Ini tidak akan cukup untuk melakukan ini, dan meskipun. NET memperkenalkan IDispose sebagai destruktor yang dikelola secara manual, dan menggunakan blok sebagai upaya untuk membuat manajemen manual itu sedikit lebih mudah, itu masih bukan sesuatu yang ingin Anda lakukan dalam praktek .

gbjbaanb
sumber
4

Dari sudut pandang saya perbedaan utama adalah bahwa destruktor di c ++ adalah mekanisme implisit (secara otomatis dipanggil) untuk melepaskan sumber daya yang dialokasikan sementara percobaan ... akhirnya dapat digunakan sebagai mekanisme eksplisit untuk melakukan itu.

Dalam program c ++, programmer bertanggung jawab untuk mengeluarkan sumber daya yang dialokasikan. Ini biasanya diimplementasikan dalam destruktor kelas dan dilakukan segera ketika suatu variabel keluar dari ruang lingkup atau atau ketika delete dipanggil.

Ketika dalam c ++ variabel lokal dari sebuah kelas dibuat tanpa menggunakan newsumber daya dari instance yang dibebaskan secara implisit oleh destruktor ketika ada pengecualian.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

Di java, c # dan sistem lain dengan manajemen memori otomatis, pengumpul sampah sistem memutuskan kapan instance kelas dihancurkan.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Tidak ada mekanisme tersirat untuk itu sehingga Anda harus memprogram ini secara eksplisit menggunakan try akhirnya

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}
k3b
sumber
Dalam C ++, mengapa destructor dipanggil secara otomatis hanya dengan objek stack dan tidak dengan objek heap jika ada pengecualian?
Giorgio
@Giorgio Karena sumber daya tumpukan tinggal di ruang memori yang tidak langsung terkait dengan tumpukan panggilan. Misalnya, bayangkan aplikasi multithreaded dengan 2 utas, Adan B. Jika satu utas dilempar, A'stransaksi penggulungan kembali tidak boleh menghancurkan sumber daya yang dialokasikan B, misalnya - status utas tidak tergantung satu sama lain, dan memori yang terus-menerus hidup di tumpukan tidak tergantung pada keduanya. Namun, biasanya di C ++, memori tumpukan masih terikat ke objek di tumpukan.
@Giorgio Sebagai contoh, sebuah std::vectorobjek mungkin hidup di stack tetapi menunjuk ke memori di heap - baik objek vektor (di stack) dan isinya (di heap) akan dibatalkan alokasi selama stack bersantai dalam kasus itu, karena menghancurkan vektor pada stack akan memanggil destructor yang membebaskan memori terkait pada heap (dan juga menghancurkan elemen heap itu). Biasanya untuk keamanan pengecualian, sebagian besar objek C ++ hidup di stack, bahkan jika mereka hanya menangani menunjuk ke memori di heap, mengotomatiskan proses membebaskan kedua heap dan memori stack di stack, bersantai.
4

Senang Anda memposting ini sebagai pertanyaan. :)

Saya mencoba mengatakan bahwa destructor dan finallysecara konseptual berbeda:

  • Destructors adalah untuk melepaskan sumber daya ( data )
  • finallyadalah untuk kembali ke penelepon ( kontrol )

Pertimbangkan, katakanlah, pseudo-code hipotetis ini:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallydi sini adalah menyelesaikan masalah kontrol sepenuhnya dan bukan masalah manajemen sumber daya.
Tidak masuk akal untuk melakukan itu di destructor karena berbagai alasan:

  • Tidak ada sesuatu yang "diperoleh" atau "diciptakan"
  • Kegagalan untuk mencetak ke file log tidak akan menyebabkan kebocoran sumber daya, korupsi data, dll. (Dengan asumsi bahwa file log di sini tidak dimasukkan kembali ke dalam program di tempat lain)
  • Adalah sah untuk logfile.printgagal, sedangkan kehancuran (secara konseptual) tidak bisa gagal

Berikut contoh lain, kali ini seperti di Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

Dalam contoh di atas, sekali lagi, tidak ada sumber daya yang akan dirilis.
Bahkan, finallyblok memperoleh sumber daya secara internal untuk mencapai tujuannya, yang berpotensi gagal. Oleh karena itu, tidak masuk akal untuk menggunakan destruktor (jika Javascript memilikinya).

Di sisi lain, dalam contoh ini:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallysedang menghancurkan sumber daya b,. Ini masalah data. Masalahnya bukan tentang mengembalikan kontrol ke pemanggil, tetapi tentang menghindari kebocoran sumber daya.
Kegagalan bukanlah suatu pilihan, dan seharusnya (secara konseptual) tidak pernah terjadi.
Setiap rilis bharus dipasangkan dengan akuisisi, dan masuk akal untuk menggunakan RAII.

Dengan kata lain, hanya karena Anda dapat menggunakan salah satu untuk mensimulasikan baik itu tidak berarti keduanya adalah satu dan masalah yang sama atau bahwa keduanya adalah solusi yang tepat untuk kedua masalah.

pengguna541686
sumber
Terima kasih. Saya tidak setuju, tapi hei :-) Saya pikir saya akan dapat menambahkan jawaban pandangan yang berlawanan secara menyeluruh di hari-hari berikutnya ...
Martin Ba
2
Bagaimana fakta yang finallysebagian besar digunakan untuk melepaskan faktor sumber daya (non-memori) ke dalam ini?
Bart van Ingen Schenau
1
@ BartvanIngenSchenau: Saya tidak pernah berpendapat bahwa bahasa apa pun yang ada saat ini memiliki filosofi atau implementasi yang sesuai dengan apa yang saya jelaskan. Orang belum selesai menemukan semua yang mungkin ada. Saya hanya berpendapat bahwa akan ada nilai dalam memisahkan kedua gagasan karena mereka adalah ide yang berbeda dan memiliki kasus penggunaan yang berbeda. Untuk memuaskan keingintahuan Anda, saya yakin D memiliki keduanya. Mungkin ada bahasa lain juga. Saya tidak menganggapnya relevan, dan saya tidak peduli mengapa misalnya Jawa yang mendukung finally.
user541686
1
Contoh praktis yang saya temui dalam JavaScript adalah fungsi yang sementara mengubah pointer mouse ke jam pasir selama beberapa operasi yang panjang (yang mungkin membuang pengecualian), dan kemudian mengubahnya kembali normal di finallyklausa. Pandangan dunia C ++ akan memperkenalkan kelas yang mengelola “sumber daya” tugas ini ke variabel pseudo-global. Pengertian konseptual apa itu? Tetapi destruktor adalah palu C ++ untuk eksekusi kode akhir blok yang diperlukan.
dan04
1
@ dan04: Terima kasih banyak, itu contoh sempurna untuk ini. Aku bersumpah aku akan menemukan begitu banyak situasi di mana RAII tidak masuk akal, tetapi aku kesulitan memikirkannya.
user541686
1

Jawaban k3b benar-benar mengucapkannya dengan baik:

destructor di c ++ adalah mekanisme implisit (secara otomatis dipanggil) untuk melepaskan sumber daya yang dialokasikan sementara percobaan ... akhirnya dapat digunakan sebagai mekanisme eksplisit untuk melakukan itu.

Adapun "sumber daya", saya suka merujuk ke Jon Kalb: RAII harus berarti Tanggung Jawab Akuisisi adalah Inisialisasi .

Bagaimanapun, seperti untuk implisit vs eksplisit ini tampaknya benar-benar itu:

  • D'tor adalah alat untuk menentukan operasi apa yang akan terjadi - secara implisit - ketika masa berakhirnya suatu objek (yang sering kali bertepatan dengan end-of-scope)
  • Akhirnya-blok adalah alat untuk mendefinisikan - secara eksplisit - operasi apa yang akan terjadi pada end-of-scope.
  • Plus, secara teknis, Anda selalu diizinkan untuk melempar dari akhirnya, tetapi lihat di bawah.

Saya pikir itu saja untuk bagian konseptual, ...


... sekarang ada IMHO beberapa detail menarik:

Saya juga tidak berpikir bahwa c'tor / d'tor perlu secara konseptual "memperoleh" atau "membuat" apa pun, terlepas dari tanggung jawab untuk menjalankan beberapa kode dalam destruktor. Yang akhirnya juga: menjalankan beberapa kode.

Dan sementara kode pada akhirnya blok tentu saja dapat mengeluarkan pengecualian, itu tidak cukup perbedaan bagi saya untuk mengatakan bahwa mereka secara konseptual berbeda di atas eksplisit vs implisit.

(Plus, saya tidak yakin sama sekali bahwa kode "baik" harus dibuang dari akhirnya - mungkin itu pertanyaan lain untuk dirinya sendiri.)

Martin Ba
sumber
Apa pendapat Anda tentang contoh Javascript saya?
user541686
Mengenai argumen Anda yang lain: "Apakah kita benar-benar ingin mencatat hal yang sama tanpa menghiraukannya?" Ya, itu hanya contoh dan Anda agak kehilangan intinya, dan ya, tidak ada yang pernah dilarang untuk memasukkan detail yang lebih spesifik untuk setiap kasus. Intinya di sini adalah bahwa Anda tentu tidak dapat mengklaim tidak pernah ada situasi di mana Anda ingin mencatat sesuatu yang umum bagi keduanya. Beberapa entri log bersifat umum, beberapa spesifik; kamu ingin keduanya. Dan lagi, Anda benar-benar kehilangan titik dengan berfokus pada pencatatan. Memotivasi contoh 10-baris sulit; tolong jangan sampai ketinggalan intinya.
user541686
Anda tidak pernah membahas ini ...
user541686
@Mehrdad - Saya tidak membahas contoh javascript Anda karena itu akan membawa saya halaman lain untuk membahas apa yang saya pikirkan. (Saya mencoba, tetapi butuh waktu lama bagi saya untuk mengucapkan sesuatu yang masuk akal sehingga saya melewatkannya :-)
Martin Ba
@Mehrdad - untuk poin Anda yang lain - sepertinya kami harus setuju untuk tidak setuju. Saya melihat di mana Anda bertujuan dengan perbedaan, tapi saya hanya tidak yakin bahwa mereka adalah sesuatu yang berbeda secara konseptual: Terutama karena saya sebagian besar di kamp yang berpikir melemparkan pada akhirnya adalah ide yang sangat buruk ( catatan : saya juga pikirkan dalam observercontoh Anda bahwa melempar akan ada ide yang sangat buruk.) Jangan ragu untuk membuka obrolan, jika Anda ingin membahas ini lebih lanjut. Pasti menyenangkan memikirkan argumen Anda. Bersulang.
Martin Ba