Bagaimana menangani kasus kegagalan dalam konstruktor kelas C ++?

21

Saya memiliki kelas CPP yang konstruktornya melakukan beberapa operasi. Beberapa operasi ini mungkin gagal. Saya tahu bahwa konstruktor tidak mengembalikan apa pun.

Pertanyaan saya adalah,

  1. Apakah diizinkan melakukan beberapa operasi selain yang menginisialisasi anggota dalam konstruktor?

  2. Apakah mungkin untuk memberi tahu fungsi panggilan bahwa beberapa operasi dalam konstruktor telah gagal?

  3. Bisakah saya membuat new ClassName()pengembalian NULL jika beberapa kesalahan terjadi di konstruktor?

MayurK
sumber
22
Anda bisa melempar pengecualian dari dalam konstruktor. Ini adalah pola yang sepenuhnya valid.
Andy
1
Anda mungkin harus melihat beberapa pola kreasi dari GoF . Saya merekomendasikan pola pabrik.
SpaceTrucker
2
Contoh umum # 1 adalah validasi data. Yaitu jika Anda memiliki kelas Square, dengan konstruktor yang mengambil satu parameter, panjang sisi, Anda ingin memeriksa apakah nilai itu lebih besar dari 0.
David berkata Reinstate Monica
1
Untuk pertanyaan pertama, izinkan saya memperingatkan Anda bahwa fungsi virtual dapat berperilaku tidak sengaja dalam konstruktor. Sama dengan dekonstruksi. Waspadalah memanggil seperti itu.
1
# 3 - Mengapa Anda ingin mengembalikan NULL? Salah satu manfaat OO adalah TIDAK harus memeriksa nilai kembali. Tangkap () pengecualian potensial yang sesuai.
MrWificent

Jawaban:

42
  1. Ya, meskipun beberapa standar pengkodean mungkin melarangnya.

  2. Iya nih. Cara yang disarankan adalah dengan melemparkan pengecualian. Atau, Anda dapat menyimpan informasi kesalahan di dalam objek dan memberikan metode untuk mengakses informasi ini.

  3. Tidak.

Sebastian Redl
sumber
4
Kecuali objek masih dalam keadaan valid meskipun beberapa bagian dari argumen konstruktor tidak memenuhi persyaratan dan dengan demikian ditandai sebagai kesalahan, 2) benar-benar tidak direkomendasikan untuk dilakukan. Lebih baik bila suatu objek ada dalam keadaan valid atau tidak ada sama sekali.
Andy
@ Davidvider setuju, lihat di sini: stackoverflow.com/questions/77639/... Tetapi beberapa pedoman pengkodean melarang pengecualian, yang bermasalah untuk konstruktor.
Sebastian Redl
Entah bagaimana saya sudah memberi Anda suara keras untuk jawaban itu, Sebastian. Menarik. : D
Andy
10
@oxox Tidak, tidak. Baru Anda yang ditimpa dipanggil untuk mengalokasikan memori, tetapi panggilan ke konstruktor dilakukan oleh kompilator setelah operator Anda kembali, yang berarti Anda tidak bisa mendapatkan kesalahan. Itu dengan asumsi baru disebut sama sekali; itu bukan untuk objek yang dialokasikan stack, yang seharusnya sebagian besar dari mereka.
Sebastian Redl
1
Untuk # 1, RAII adalah contoh umum di mana melakukan lebih banyak dalam konstruktor mungkin diperlukan.
Eric
20

Anda bisa membuat metode statis yang melakukan perhitungan dan mengembalikan objek jika sukses atau tidak.

Bergantung pada bagaimana konstruksi objek ini dilakukan, mungkin lebih baik untuk membuat objek lain yang memungkinkan konstruksi objek dalam metode yang tidak statis.

Memanggil konstruktor secara tidak langsung sering disebut sebagai "pabrik".

Ini juga akan memungkinkan Anda untuk mengembalikan objek-nol, yang mungkin merupakan solusi yang lebih baik daripada mengembalikan null.

batal
sumber
@Null terima kasih! Sayangnya tidak dapat menerima dua jawaban di sini :( Kalau tidak, saya akan menerima jawaban ini juga !! Terima kasih lagi!
MayurK
@MayurK tidak ada kekhawatiran, jawaban yang diterima tidak untuk menandai dengan jawaban yang benar, tapi satu yang bekerja untuk Anda.
null
3
@null: Di C ++, Anda tidak bisa kembali begitu saja NULL. Misalnya, int foo() { return NULLAnda benar-benar akan mengembalikan 0(nol), objek integer. Jika std::string foo() { return NULL; }Anda tidak sengaja menelepon std::string::string((const char*)NULL)yang merupakan Perilaku Tidak Terdefinisi (NULL tidak mengarah ke string yang diakhiri \ 0).
MSalters
3
std :: opsional mungkin jauh tetapi Anda selalu bisa menggunakan boost :: opsional jika Anda ingin pergi ke sana.
Sean Burton
1
@ Vld: Di C ++, objek tidak terbatas pada tipe kelas. Dan dengan pemrograman generik, tidak jarang berakhir dengan pabrik int. Misalnya std::allocator<int>adalah pabrik yang sangat waras.
MSalters
5

@SebastianRedl sudah memberikan jawaban langsung yang sederhana, tetapi beberapa penjelasan tambahan mungkin berguna.

TL; DR = ada aturan gaya untuk membuat konstruktor tetap sederhana, ada alasannya, tetapi alasan itu kebanyakan berhubungan dengan gaya pengkodean yang bersejarah (atau hanya buruk). Penanganan pengecualian dalam konstruktor didefinisikan dengan baik, dan destruktor masih akan dipanggil untuk variabel dan anggota lokal yang sepenuhnya dibangun, yang berarti tidak boleh ada masalah dalam kode C ++ idiomatik. Aturan gaya tetap ada, tetapi biasanya itu bukan masalah - tidak semua inisialisasi harus ada di konstruktor, dan khususnya tidak harus konstruktor itu.


Ini adalah aturan gaya umum bahwa konstruktor harus melakukan minimum absolut yang mereka bisa untuk mengatur keadaan valid yang ditentukan. Jika inisialisasi Anda lebih kompleks, itu harus ditangani di luar konstruktor. Jika tidak ada nilai murah untuk diinisialisasi yang bisa disiapkan oleh konstruktor Anda, Anda harus melemahkan invarants yang diberlakukan oleh kelas Anda untuk menambahkannya. Sebagai contoh jika mengalokasikan penyimpanan untuk kelas Anda untuk mengelola terlalu mahal, tambahkan status null yang belum dialokasikan, karena tentu saja memiliki status case khusus seperti null tidak pernah menyebabkan masalah pada siapa pun. Ahem.

Meskipun umum, tentu dalam bentuk ekstrem ini sangat jauh dari absolut. Secara khusus, seperti yang ditunjukkan oleh sarkasme saya, saya berada di kamp yang mengatakan bahwa pelemahan invarian hampir selalu merupakan harga yang terlalu tinggi. Namun, ada alasan di balik aturan gaya, dan ada cara untuk memiliki konstruktor minimal dan invarian yang kuat.

Alasannya berkaitan dengan pembersihan destruktor otomatis, khususnya dalam menghadapi pengecualian. Pada dasarnya, harus ada titik yang terdefinisi dengan baik ketika kompiler menjadi bertanggung jawab untuk memanggil destruktor. Saat Anda masih dalam panggilan konstruktor, objek tidak harus sepenuhnya dibangun, jadi tidak sah untuk memanggil destruktor untuk objek itu. Oleh karena itu, tanggung jawab untuk merusak objek hanya ditransfer ke kompiler ketika konstruktor berhasil menyelesaikan. Ini dikenal sebagai RAII (Alokasi Sumber Daya Adalah Inisialisasi) yang sebenarnya bukan nama terbaik.

Jika lemparan pengecualian terjadi di dalam konstruktor, bagian apa pun yang dibangun perlu dibersihkan secara eksplisit, biasanya dalam a try .. catch.

Namun, komponen objek yang telah berhasil dibangun sudah menjadi tanggung jawab penyusun. Ini berarti bahwa dalam praktiknya, itu bukan masalah besar. misalnya

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

Badan konstruktor ini kosong. Selama konstruktor untuk base1, member2dan member3adalah pengecualian aman, tidak ada yang perlu khawatir tentang. Misalnya, jika konstruktor member2melempar, konstruktor tersebut bertanggung jawab untuk membersihkan dirinya sendiri. Pangkalan base1sudah benar-benar dibangun, sehingga destruktornya akan secara otomatis dipanggil. member3bahkan tidak pernah sebagian dibangun, jadi tidak perlu pembersihan.

Bahkan ketika ada badan, variabel lokal yang dibangun sepenuhnya sebelum pengecualian dilemparkan akan secara otomatis dihancurkan, sama seperti fungsi lainnya. Badan konstruktor yang menyulap pointer mentah, atau "memiliki" semacam keadaan implisit (disimpan di tempat lain) - biasanya berarti panggilan fungsi begin / memperoleh harus dicocokkan dengan panggilan akhir / rilis - dapat menyebabkan pengecualian masalah keselamatan, tetapi masalah sebenarnya ada gagal mengelola sumber daya dengan benar melalui kelas. Misalnya jika Anda mengganti pointer mentah dengan unique_ptrdalam konstruktor, destruktor untuk unique_ptrakan dipanggil secara otomatis jika diperlukan.

Masih ada alasan lain yang orang berikan untuk memilih konstruktor do-the-minimum. Salah satunya adalah karena aturan gaya ada, banyak orang menganggap panggilan konstruktor itu murah. Salah satu cara untuk mendapatkannya, namun masih memiliki invarian yang kuat, adalah dengan memiliki kelas pabrik / pembangun terpisah yang memiliki invarian yang melemah, dan yang menetapkan nilai awal yang diperlukan menggunakan (kemungkinan banyak) panggilan fungsi anggota normal. Setelah Anda memiliki keadaan awal yang Anda butuhkan, berikan objek itu sebagai argumen kepada konstruktor untuk kelas dengan invarian yang kuat. Itu bisa "mencuri nyali" objek invarian lemah - pindahkan semantik - yang merupakan operasi yang murah (dan biasanya noexcept).

Dan tentu saja Anda dapat membungkusnya dalam suatu make_whatever ()fungsi, jadi penelepon dari fungsi itu tidak perlu melihat instance kelas yang dilemahkan-invarian.

Steve314
sumber
Paragraf di mana Anda menulis "Saat Anda masih dalam panggilan konstruktor, objek belum tentu sepenuhnya dibangun, jadi tidak valid untuk memanggil destruktor untuk objek itu. Oleh karena itu, tanggung jawab untuk merusak objek hanya transfer ke kompiler. ketika konstruktor berhasil diselesaikan "benar-benar dapat menggunakan pembaruan tentang mendelegasikan konstruktor. Objek sepenuhnya dibangun ketika konstruktor yang paling diturunkan selesai, dan destruktor akan dipanggil jika pengecualian terjadi di dalam konstruktor yang mendelegasikan.
Ben Voigt
Dengan demikian, konstruktor "do-the-minimum" bisa bersifat pribadi, dan fungsi "make_whthing ()" bisa menjadi konstruktor lain yang memanggil yang pribadi.
Ben Voigt
Ini bukan definisi RAII yang saya kenal. Pemahaman saya tentang RAII adalah untuk secara sengaja memperoleh sumber daya di (dan hanya di) konstruktor objek dan melepaskannya di destruktornya. Dengan cara ini, objek dapat digunakan pada stack untuk secara otomatis mengelola akuisisi dan pelepasan sumber daya yang di-enkapsulasi. Contoh klasik adalah kunci yang memperoleh mutex ketika dibangun dan melepaskannya pada kehancuran.
Eric
1
@ Eric - Ya, ini benar-benar praktik standar - praktik standar yang biasa disebut RAII. Bukan hanya saya yang memperluas definisi - bahkan Stroustrup, dalam beberapa pembicaraan. Ya, RAII adalah tentang menghubungkan tautan siklus hidup sumber daya ke siklus hidup objek, model mental yang menjadi kepemilikan.
Steve314
1
@ Eric - balasan sebelumnya dihapus karena dijelaskan dengan buruk. Pokoknya, objek itu sendiri adalah sumber daya yang bisa dimiliki. Semuanya harus memiliki pemilik, dalam rantai hingga mainfungsi atau variabel statis / global. Objek yang dialokasikan menggunakan new, tidak dimiliki hingga Anda menetapkan tanggung jawab itu, tetapi pointer pintar memiliki objek yang dialokasikan tumpukan yang dirujuknya, dan wadah memiliki struktur datanya. Pemilik dapat memilih untuk menghapus lebih awal, pemilik destructor pada akhirnya bertanggung jawab.
Steve314