Pengecualian memori dinamis dan konstruktor

Awal hari ini saya menemukan blok fungsi try-catch (sebenarnya dari di sini) dan kemudian melanjutkan sedikit penelitian - rupanya kegunaan utamanya adalah menangkap pengecualian yang dimasukkan oleh daftar penginisialisasi konstruktor.

Bagaimanapun, ini membuat saya berpikir tentang konstruktor yang gagal dan saya telah sampai pada tahap di mana saya hanya memerlukan sedikit klarifikasi. Ini semua hanyalah upaya saya untuk mempelajari lebih lanjut tentang bahasa tersebut, jadi saya tidak memiliki contoh praktis, tapi begini...


Diberikan contoh kode ini:

class A
{
private:
    B b
    C *c;    //classes B, C & D omitted for brevity as not really relevant
    D d;
public
    A(int x, int y, int z)
};

A::A(int x, int y, int z)
try
    : b( x )
    , c( new C(y) )
    , d( z )
{
    //omitted
}
catch(...)
{
    //omitted
}

Apa yang terjadi dalam kasus ini:

  1. Inisialisasi b menimbulkan pengecualian.
  2. Inisialisasi c memunculkan pengecualian.
  3. Inisialisasi d memunculkan pengecualian.

Secara khusus, saya ingin tahu setidaknya:

  • apa yang akan/mungkin menyebabkan kebocoran memori dari new C(y). Aku hanya memikirkan 3? (lihat di sini)
  • bisakah kamu delete b saja yang menangkapnya? Apakah berbahaya pada kasus 1 dan 2?

Jelas, menurut saya hal paling aman untuk dilakukan adalah menjadikan c sebagai penunjuk cerdas. Namun jika kita mengabaikan pilihan tersebut untuk saat ini, tindakan apa yang terbaik?

Apakah aman untuk menyetel c ke NULL di penginisialisasi, lalu melakukan panggilan ke new di badan konstruktor?

Itu berarti delete c harus ditempatkan di catch jika ada sesuatu yang lain yang muncul di badan konstruktor? Apakah ada masalah keamanan saat melakukan hal itu (yaitu, jika c = new C(y); itu sendiri yang melempar)?


person DMA57361    schedule 05.07.2010    source sumber
comment
Anda tidak dapat bergantung pada pengaturan c ke NULL karena jika b memunculkan pengecualian, c belum akan ditetapkan ke NULL pada saat itu. Seperti yang Anda katakan, gunakan penunjuk cerdas.   -  person 5ound    schedule 05.07.2010
comment
@5ound - sangat poin bagus yang belum pernah saya pikirkan.   -  person DMA57361    schedule 05.07.2010


Jawaban (3)


Blok fungsi coba/tangkap tidak disukai, sama seperti goto --mungkin ada beberapa kasus sudut yang masuk akal, namun lebih baik dihindari: ketika sebuah objek gagal dibuat, hal terbaik yang dapat Anda lakukan adalah gagal dan gagal dengan cepat.

Pada pertanyaan spesifik Anda, ketika pengecualian dimasukkan ke dalam konstruktor, semua subobjek yang dibangun sepenuhnya akan dimusnahkan. Itu berarti bahwa dalam kasus b itu akan dihancurkan, dalam kasus c, itu adalah penunjuk mentah, tidak ada yang dilakukan. Solusi paling sederhana adalah mengubah c menjadi penunjuk cerdas yang menangani alokasi memori. Dengan begitu, jika d melempar, smart pointer akan dimusnahkan dan objek dilepaskan. Ini tidak terkait dengan blok coba/tangkap, melainkan cara kerja konstruktor.

Secara umum juga tidak aman untuk menghapus pointer dari blok catch, karena tidak ada jaminan nilai sebenarnya dari pointer sebelum inisialisasi dilakukan. Artinya, jika b atau c dilempar, mungkin c bukan 0, tetapi juga bukan penunjuk yang valid, dan menghapusnya akan menjadi perilaku yang tidak terdefinisi. Seperti biasa ada kasus, seolah-olah Anda memiliki jaminan bahwa b atau c tidak akan melempar, dan dengan asumsi bahwa bad_alloc bukan sesuatu yang biasanya Anda pulihkan, maka itu mungkin aman.

Perhatikan bahwa jika Anda perlu menyimpan pointer dengan pointer mentah karena alasan tertentu, lebih baik menginisialisasinya ke 0 dalam daftar inisialisasi, dan kemudian membuat objek di dalam blok konstruksi untuk menghindari masalah ini. Ingat juga bahwa menyimpan lebih dari satu sumber daya (mentah) secara langsung dalam satu kelas membuat sangat sulit untuk memastikan bahwa tidak ada sumber daya yang bocor -- jika sumber daya pertama dibuat dan ditetapkan, dan sumber daya kedua gagal, destruktor tidak akan dipanggil dan sumber daya akan bocor. Sekali lagi, jika Anda dapat menggunakan penunjuk cerdas, satu masalah yang harus Anda atasi akan berkurang.

person David Rodríguez - dribeas    schedule 05.07.2010
comment
Ya, sepertinya semakin saya memikirkan dan membaca tentang hal ini, semakin tidak banyak membantu. - person DMA57361; 05.07.2010
comment
Klaim blok coba/tangkap Anda tidak disukai, sama seperti goto... hanyalah omong kosong. try/catch adalah fitur bahasa yang digunakan untuk menulis kode dengan penanganan kesalahan yang tepat dalam konteks pengecualian. Bagaimana lagi Anda mengharapkan pengecualian ditangani? - person hkaiser; 05.07.2010
comment
@hkaiser: telah terjadi kesalahpahaman di sini, yang entah bagaimana tidak disukai adalah blok coba/tangkap tingkat fungsi: void f() try { ... } catch () {}. Fitur tersebut telah ditambahkan ke bahasa tersebut dan tidak dikenal serta tidak direkomendasikan. Saya telah mengklarifikasi maksudnya --atau setidaknya mencoba-- sekarang. Blok coba/tangkap reguler fungsi di dalam, seperti yang Anda nyatakan dengan benar, adalah fitur bahasa untuk penanganan kesalahan. Saya akan mencoba mencari referensi dari sumber terkenal sesampainya di rumah. Ini yang pertama: GOTW#66 - person David Rodríguez - dribeas; 05.07.2010
comment
@David - tautan di komentar Anda cukup berguna - dan inilah kutipan dari bagian 'moral' (antara 3 & 4), yang menurut saya memberikan jawabannya: ...atau new[] di badan konstruktor tempatnya dapat dibersihkan dengan aman menggunakan blok percobaan lokal atau lainnya. Bagi saya itu terdengar seperti solusi terbaik (tidak termasuk petunjuk cerdas) - lakukan setiap new yang diperlukan di badan konstruktor di dalamnya, bersarang, coba/tangkap - maka tangkapan dapat membersihkan semua upaya sebelumnya, lalu membuang sesuatu. - person DMA57361; 05.07.2010
comment
@ DMA57361: solusi terbaik adalah menggunakan manajer cerdas untuk setiap sumber daya: penunjuk cerdas untuk objek baru, std::vector untuk array yang dialokasikan secara dinamis, dll... menggunakan try/catch untuk menangani pelepasan sumber daya sama dengan menggunakan a palu untuk melakukan operasi, mungkin berhasil, tetapi tidak perlu rumit dan tidak mungkin berhasil dalam jangka waktu lama (pemeliharaan...) - person Matthieu M.; 06.07.2010
comment
@Matthiew M - Oh, saya setuju sepenuhnya. Saya hanya mencoba untuk mendapatkan pemahaman yang lebih baik tentang apa yang tampaknya (atau lebih tepatnya adalah) area yang agak rumit. Dan, pada titik tertentu, beberapa kelas harus mengatasi masalah ini? Petunjuk cerdas dan sejenisnya yang kita andalkan harus menangani masalah ini, karena mereka tidak dapat menggunakan konstruksi ini sendiri - tapi saya rasa ini sebagian besar berada di dalam inti perpustakaan, di mana sebagian besar dari kita tidak perlu terlalu khawatir. tentang itu... - person DMA57361; 06.07.2010
comment
@ DMA57361:*...beberapa kelas akan mengatasi masalah ini...* Tidak juga, smart pointer itu sendiri cukup sederhana dan tidak perlu menangani ini. Sumber daya dibuat secara eksternal, jika pembuatannya gagal maka secara otomatis dibersihkan. Kemudian diteruskan ke smart pointer, dan smart pointer hanya menyalin pointer --cannot throw. Kalau nanti ada yang lempar sebenarnya tidak masalah. Kelas yang mengimplementasikan RAII yang telah dibangun sepenuhnya akan dimusnahkan. Destruktor dapat melakukan pembersihan dengan cara yang paling sederhana. - person David Rodríguez - dribeas; 06.07.2010

Anda tidak dapat melakukan apa pun di pengendali fungsi-coba-blok, kecuali menerjemahkan satu pengecualian ke pengecualian lainnya. Anda tidak dapat mencegah pelemparan pengecualian. Anda tidak dapat melakukan apa pun terhadap anggota kelas. Jadi tidak, Anda tidak dapat melakukan delete c di blok fungsi-coba konstruktor.

Kelas dasar yang dibangun sepenuhnya dan anggota suatu objek harus dihancurkan sebelum memasuki pengendali blok fungsi-coba dari konstruktor atau destruktor untuk objek tersebut.

Dan

Pengecualian yang saat ini ditangani akan dilempar kembali jika kontrol mencapai akhir dari pengendali blok fungsi-coba konstruktor atau destruktor. Jika tidak, suatu fungsi akan kembali ketika kontrol mencapai akhir dari suatu pengendali untuk blok-fungsi-coba (6.6.3). Mengalir dari akhir blok fungsi-coba setara dengan pengembalian tanpa nilai; ini menghasilkan perilaku yang tidak terdefinisi dalam fungsi pengembalian nilai.

(15.3 [kecuali.handle] dari Standar C++)

person atzz    schedule 05.07.2010
comment
Jadi, pada dasarnya, objek tersebut dihancurkan (secara efektif) sebagai bagian dari pelepasan yang terjadi sebelum catch dimulai. Jadi, di catch, objek saat ini tidak ada; Anda tidak dapat mencoba membersihkan objek yang tidak ada di sana. - person DMA57361; 05.07.2010
comment
@ DMA57361 - ya. Pemahaman saya adalah bahwa function-try-block ditambahkan ke bahasa hanya dengan tujuan terjemahan pengecualian di konstruktor, untuk memfasilitasi penggunaan bersama kelas dari perpustakaan independen. Anda juga dapat memasukkan logging di sana. Cukup sekian... - person atzz; 05.07.2010

Untuk menjawab pertanyaan Anda yang sebenarnya, setiap kali pengecualian terjadi selama konstruksi, eksekusi berhenti di sana (objek yang sedang dibangun tidak pernah dibuat) dan kontrol diteruskan ke pengendali pengecualian terdekat (catch).

Anda benar bahwa kebocoran memori hanya akan terjadi ketika d menyebabkan pengecualian, dan dalam hal ini, catch bagian luar perlu dibersihkan.

Jika new C sendiri dilempar, objek tersebut tidak akan pernah dibuat, jadi Anda tidak perlu khawatir untuk menghapusnya.

person casablanca    schedule 05.07.2010
comment
Adakah saran bagaimana cara membersihkan hasil tangkapan dengan aman? Saya merasa itu tidak bisa, dan Anda mendapatkan kemungkinan kebocoran memori atau risiko menghapus pointer yang tidak diinisialisasi. - person DMA57361; 05.07.2010
comment
Di satu sisi, Anda benar - ini akan menyebabkan situasi yang canggung dan Anda tidak selalu dapat menjamin solusi yang tepat. Jika b, c dan d semuanya memunculkan pengecualian yang berbeda, Anda dapat mengetahui di mana tepatnya kegagalan terjadi dan melakukan pembersihan yang sesuai. - person casablanca; 05.07.2010