C ++ tampaknya lebih suka menggunakan pengecualian lebih sering.
Saya akan menyarankan sebenarnya kurang dari Objective-C dalam beberapa hal karena perpustakaan standar C ++ umumnya tidak akan membuang kesalahan programmer seperti akses out-of-bound urutan akses acak dalam bentuk desain kasus yang paling umum (dalam operator[]
, yaitu) atau mencoba dereference iterator yang tidak valid. Bahasa tidak melempar mengakses array di luar batas, atau mendereferensi pointer nol, atau semacamnya.
Mengambil kesalahan programmer sebagian besar dari persamaan penanganan pengecualian sebenarnya menghilangkan kategori kesalahan yang sangat besar yang sering ditanggapi oleh bahasa lain throwing
. C ++ cenderung assert
(yang tidak dikompilasi dalam rilis / produksi build, hanya debug membangun) atau hanya kesalahan (sering crash) dalam kasus seperti itu, mungkin sebagian karena bahasa tidak ingin membebankan biaya pemeriksaan runtime seperti seperti yang diperlukan untuk mendeteksi kesalahan programmer tersebut kecuali programmer secara khusus ingin membayar biaya dengan menulis kode yang melakukan pemeriksaan sendiri.
Sutter bahkan mendorong untuk menghindari pengecualian dalam kasus seperti itu dalam C ++ Standar Pengkodean:
Kerugian utama menggunakan pengecualian untuk melaporkan kesalahan pemrograman adalah bahwa Anda tidak benar-benar ingin terjadi penumpukan gulungan ketika Anda ingin debugger diluncurkan pada baris yang tepat di mana pelanggaran terdeteksi, dengan status garis utuh. Singkatnya: Ada kesalahan yang Anda tahu mungkin terjadi (lihat Item 69 hingga 75). Untuk semua hal lain yang tidak boleh, dan itu kesalahan programmer jika itu ada assert
.
Aturan itu tidak harus diatur dalam batu. Dalam beberapa kasus yang lebih kritis, mungkin lebih baik menggunakan, katakanlah, pembungkus dan standar pengkodean yang secara seragam mencatat di mana kesalahan pemrogram terjadi dan throw
di hadapan kesalahan pemrogram seperti mencoba untuk menghormati sesuatu yang tidak valid atau mengaksesnya di luar batas, karena mungkin terlalu mahal untuk gagal pulih dalam kasus-kasus tersebut jika perangkat lunak memiliki kesempatan. Namun secara keseluruhan penggunaan bahasa yang lebih umum cenderung mendukung tidak membuang kesalahan programmer.
Pengecualian Eksternal
Di mana saya melihat pengecualian yang paling sering didorong dalam C ++ (menurut komite standar, misalnya) adalah untuk "pengecualian eksternal", seperti dalam hasil yang tidak terduga di beberapa sumber eksternal di luar program. Contoh gagal mengalokasikan memori. Lain gagal untuk membuka file penting yang diperlukan untuk menjalankan perangkat lunak. Lainnya gagal terhubung ke server yang diperlukan. Lain adalah pengguna macet tombol batalkan untuk membatalkan operasi yang jalur eksekusi kasus umum mengharapkan berhasil tidak ada gangguan eksternal ini. Semua hal ini berada di luar kendali perangkat lunak langsung dan programmer yang menulisnya. Mereka adalah hasil yang tidak terduga dari sumber eksternal yang mencegah operasi (yang harus benar-benar dianggap sebagai transaksi yang tidak dapat dibagi dalam buku saya *) untuk dapat berhasil.
Transaksi
Saya sering mendorong melihat try
blok sebagai "transaksi" karena transaksi harus berhasil secara keseluruhan atau gagal secara keseluruhan. Jika kita mencoba melakukan sesuatu dan gagal di tengah jalan, maka setiap efek samping / mutasi yang dibuat ke keadaan program umumnya perlu digulung kembali untuk mengembalikan sistem ke keadaan yang valid seolah-olah transaksi tidak pernah dieksekusi sama sekali, sama seperti RDBMS yang gagal untuk memproses permintaan setengah jalan seharusnya tidak mengganggu integritas database. Jika Anda mengubah status program secara langsung dalam transaksi tersebut, maka Anda harus "membatalkan" itu saat menemukan kesalahan (dan di sini ruang lingkup penjaga dapat berguna dengan RAII).
Alternatif yang jauh lebih sederhana adalah jangan mengubah status program semula; Anda dapat mengubah salinannya dan kemudian, jika berhasil, tukar salinan dengan yang asli (pastikan swap tidak dapat dilemparkan). Jika gagal, buang salinannya. Ini juga berlaku bahkan jika Anda tidak menggunakan pengecualian untuk penanganan kesalahan secara umum. Pola pikir "transaksional" adalah kunci untuk pemulihan yang tepat jika mutasi keadaan program telah terjadi sebelum menemukan kesalahan. Entah berhasil secara keseluruhan atau gagal secara keseluruhan. Itu tidak setengah berhasil membuat mutasinya.
Ini adalah salah satu topik yang paling jarang dibahas ketika saya melihat programmer bertanya tentang bagaimana melakukan penanganan kesalahan atau pengecualian dengan benar, namun itu adalah yang paling sulit dari mereka semua untuk mendapatkan yang benar dalam setiap perangkat lunak yang ingin secara langsung mengubah keadaan program di banyak operasinya. Kemurnian dan kekekalan dapat membantu di sini untuk mencapai keselamatan pengecualian seperti halnya mereka membantu keselamatan benang, sebagai efek samping mutasi / eksternal yang tidak terjadi tidak perlu digulung kembali.
Performa
Faktor penuntun lain dalam apakah menggunakan pengecualian atau tidak adalah kinerja, dan saya tidak bermaksud dengan cara yang obsesif, mencubit, kontraproduktif. Banyak kompiler C ++ mengimplementasikan apa yang disebut "Zero-Cost Exception Handling".
Ia menawarkan overhead nol runtime untuk eksekusi bebas kesalahan, yang bahkan melampaui penanganan error nilai-kembali C. Sebagai trade-off, penyebaran pengecualian memiliki overhead yang besar.
Menurut apa yang telah saya baca tentang hal itu, itu membuat jalur eksekusi kasus umum Anda tidak memerlukan overhead (bahkan overhead yang biasanya menyertai penanganan dan penyebaran kode kesalahan gaya-C), dengan imbalan sangat mengurangi biaya menuju jalur luar biasa ( yang artinya throwing
sekarang lebih mahal dari sebelumnya).
"Mahal" agak sulit untuk dikuantifikasi tetapi, sebagai permulaan, Anda mungkin tidak ingin melempar jutaan kali dalam beberapa putaran ketat. Desain semacam ini mengasumsikan bahwa pengecualian tidak terjadi kiri dan kanan sepanjang waktu.
Non-Kesalahan
Dan titik kinerja itu membawa saya ke non-error, yang secara mengejutkan kabur jika kita melihat semua jenis bahasa lain. Tapi saya akan mengatakan, mengingat desain EH nol biaya yang disebutkan di atas, bahwa Anda hampir pasti tidak mau throw
menanggapi kunci yang tidak ditemukan dalam satu set. Karena tidak hanya itu bisa dibilang bukan kesalahan (orang yang mencari kunci mungkin telah membangun set dan berharap akan mencari kunci yang tidak selalu ada), tetapi itu akan sangat mahal dalam konteks itu.
Misalnya, fungsi persimpangan set mungkin ingin mengulang dua set dan mencari kunci yang sama. Jika gagal menemukan kunci threw
, Anda akan mengulangi dan mungkin menemukan pengecualian di setengah atau lebih dari iterasi:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
Contoh di atas benar-benar konyol dan dilebih-lebihkan, tetapi saya telah melihat, dalam kode produksi, beberapa orang yang berasal dari bahasa lain menggunakan pengecualian dalam C ++ agak seperti ini, dan saya pikir itu adalah pernyataan yang cukup praktis bahwa ini bukan penggunaan pengecualian yang tepat. dalam C ++. Petunjuk lain di atas adalah bahwa Anda akan melihat catch
blok sama sekali tidak ada hubungannya dan hanya ditulis untuk secara paksa mengabaikan pengecualian semacam itu, dan itu biasanya sebuah petunjuk (meskipun bukan penjamin) bahwa pengecualian mungkin tidak digunakan dengan sangat tepat di C ++.
Untuk jenis kasus tersebut, beberapa jenis nilai pengembalian yang menunjukkan kegagalan (mulai dari kembali false
ke iterator yang tidak valid nullptr
atau apa pun yang masuk akal dalam konteks) biasanya jauh lebih sesuai, dan juga sering lebih praktis dan produktif karena jenis kesalahan yang tidak salah. kasing biasanya tidak meminta beberapa proses tumpukan untuk mencapai catch
situs analog .
Pertanyaan
Saya harus menggunakan flag kesalahan internal jika saya memilih untuk menghindari pengecualian. Akankah itu terlalu merepotkan untuk ditangani, atau mungkin akan bekerja lebih baik daripada pengecualian? Perbandingan kedua kasus akan menjadi jawaban terbaik.
Menghindari pengecualian langsung di C ++ tampaknya sangat kontraproduktif bagi saya, kecuali jika Anda bekerja di beberapa sistem tertanam atau jenis kasus tertentu yang melarang penggunaannya (dalam hal ini Anda juga harus keluar dari cara Anda untuk menghindari semua fungsi perpustakaan dan bahasa yang sebaliknya throw
, ingin menggunakan secara ketat nothrow
new
).
Jika Anda benar-benar harus menghindari pengecualian untuk alasan apa pun (mis: bekerja melintasi batas C API dari modul yang C API Anda ekspor), banyak yang mungkin tidak setuju dengan saya, tetapi saya benar-benar menyarankan menggunakan penangan kesalahan global / status seperti OpenGL dengan glGetError()
. Anda dapat membuatnya menggunakan penyimpanan thread-lokal untuk memiliki status kesalahan unik per utas.
Alasan saya untuk itu adalah bahwa saya tidak terbiasa melihat tim di lingkungan produksi memeriksa semua kemungkinan kesalahan, sayangnya, ketika kode kesalahan dikembalikan. Jika mereka teliti, beberapa API C dapat mengalami kesalahan dengan hampir setiap panggilan API C tunggal, dan pemeriksaan menyeluruh akan membutuhkan sesuatu seperti:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... dengan hampir setiap baris kode yang memohon API yang membutuhkan pemeriksaan semacam itu. Namun saya belum beruntung bisa bekerja dengan tim yang begitu teliti. Mereka sering mengabaikan kesalahan seperti itu setengah, kadang-kadang bahkan sebagian besar waktu. Itu adalah daya tarik terbesar bagi saya untuk pengecualian. Jika kami membungkus API ini dan membuatnya seragam throw
saat menemukan kesalahan, pengecualian tidak mungkin diabaikan , dan menurut saya, dan pengalaman, di situlah letak keunggulan pengecualian.
Tetapi jika pengecualian tidak dapat digunakan, maka status kesalahan global, per-utas setidaknya memiliki keuntungan (yang sangat besar dibandingkan dengan mengembalikan kode kesalahan kepada saya) yang mungkin memiliki kesempatan untuk menangkap kesalahan yang lama sedikit lebih lambat daripada ketika itu terjadi di beberapa basis kode ceroboh bukannya langsung hilang itu dan meninggalkan kita benar-benar lupa tentang apa yang terjadi. Kesalahan mungkin terjadi beberapa baris sebelumnya, atau dalam panggilan fungsi sebelumnya, tetapi asalkan perangkat lunak belum crash, kita mungkin dapat mulai bekerja mundur dan mencari tahu di mana dan mengapa itu terjadi.
Sepertinya saya karena pointer jarang, saya harus pergi dengan flag kesalahan internal jika saya memilih untuk menghindari pengecualian.
Saya tidak akan selalu mengatakan pointer jarang. Bahkan ada metode sekarang di C ++ 11 dan seterusnya untuk mendapatkan pointer data yang mendasari wadah, dan nullptr
kata kunci baru . Biasanya dianggap tidak bijaksana untuk menggunakan pointer mentah untuk memiliki / mengelola memori jika Anda dapat menggunakan sesuatu seperti unique_ptr
sebagai gantinya mengingat betapa pentingnya untuk menyesuaikan RAII dengan adanya pengecualian. Tetapi pointer mentah yang tidak memiliki / mengelola memori tidak selalu dianggap begitu buruk (bahkan dari orang-orang seperti Sutter dan Stroustrup) dan kadang-kadang sangat praktis sebagai cara untuk menunjukkan sesuatu (bersama dengan indeks yang menunjuk pada sesuatu).
Mereka bisa dibilang tidak kalah aman dari iterator penampung standar (setidaknya dalam rilis, iterator yang tidak ada diperiksa) yang tidak akan mendeteksi jika Anda mencoba men-dereferensi mereka setelah tidak valid. C ++ masih tanpa malu-malu sedikit bahasa yang berbahaya, saya katakan, kecuali penggunaan spesifik Anda ingin membungkus semuanya dan menyembunyikan bahkan pointer mentah yang tidak memiliki. Hampir kritis dengan pengecualian bahwa sumber daya sesuai dengan RAII (yang umumnya datang tanpa biaya runtime), tetapi selain itu tidak selalu mencoba menjadi bahasa paling aman untuk digunakan dalam menghindari biaya yang tidak secara eksplisit diinginkan oleh pengembang dalam ditukar dengan sesuatu yang lain. Penggunaan yang disarankan tidak mencoba untuk melindungi Anda dari hal-hal seperti pointer menggantung dan iterator yang tidak valid, jadi untuk berbicara (kalau tidak kita akan didorong untuk menggunakanshared_ptr
semua tempat, yang Stroustrup sangat menentang). Ini mencoba melindungi Anda dari kegagalan untuk benar-benar membebaskan / melepaskan / menghancurkan / membuka / membersihkan sumber daya ketika sesuatu throws
.