Apakah menggunakan assert () di C ++ merupakan praktik yang buruk?

94

Saya cenderung menambahkan banyak pernyataan ke kode C ++ saya untuk membuat debugging lebih mudah tanpa mempengaruhi kinerja rilis build. Sekarang, assertadalah makro C murni yang dirancang tanpa memikirkan mekanisme C ++.

C ++ di sisi lain mendefinisikan std::logic_error, yang dimaksudkan untuk dilemparkan dalam kasus di mana ada kesalahan dalam logika program (karena itu namanya). Melempar sebuah instance mungkin saja merupakan alternatif yang sempurna dan lebih C ++ untuk assert.

Masalahnya adalah assertdan abortkeduanya segera menghentikan program tanpa memanggil destruktor, oleh karena itu melewatkan pembersihan, sedangkan melempar pengecualian secara manual menambah biaya runtime yang tidak perlu. Salah satu cara untuk mengatasinya akan membuat makro pernyataan sendiri SAFE_ASSERT, yang berfungsi seperti mitra C, tetapi memberikan pengecualian jika gagal.

Saya dapat memikirkan tiga pendapat tentang masalah ini:

  • Tetap berpegang pada pernyataan C. Karena program segera dihentikan, tidak masalah apakah perubahan dibuka gulungannya dengan benar. Selain itu, menggunakan #defines di C ++ sama buruknya.
  • Lemparkan pengecualian dan tangkap di main () . Mengizinkan kode untuk melewati destruktor dalam keadaan program apa pun adalah praktik yang buruk dan harus dihindari dengan cara apa pun, begitu juga panggilan ke terminate (). Jika pengecualian dilemparkan, mereka harus ditangkap.
  • Lempar pengecualian dan biarkan menghentikan program. Pengecualian untuk menghentikan program tidak apa-apa, dan karena itu NDEBUG, ini tidak akan pernah terjadi dalam build rilis. Penangkapan tidak diperlukan dan mengekspos detail implementasi kode internal ke main().

Apakah ada jawaban pasti untuk masalah ini? Ada referensi profesional?

Diedit: Melewati destruktor, tentu saja, bukanlah perilaku yang tidak ditentukan.

Fabian Knorr
sumber
22
Tidak, sungguh, logic_erroradalah kesalahan logika. Kesalahan dalam logika program disebut bug. Anda tidak memecahkan bug dengan melemparkan pengecualian.
R. Martinho Fernandes
4
Pernyataan, pengecualian, kode kesalahan. Masing-masing memiliki kasus penggunaan yang sangat berbeda, dan Anda tidak boleh menggunakan satu sama lain jika diperlukan.
Kerrek SB
5
Pastikan Anda menggunakan static_asserttempat yang sesuai jika Anda memilikinya.
Flexo
4
@ Trion Saya tidak melihat bagaimana itu membantu. Apakah Anda akan melempar std::bug?
R. Martinho Fernandes
3
@ Trion: Jangan lakukan itu. Pengecualian bukan untuk debugging. Seseorang mungkin menangkap pengecualian. Tidak perlu khawatir tentang UB saat menelepon std::abort(); itu hanya akan menaikkan sinyal yang menyebabkan proses dihentikan.
Kerrek SB

Jawaban:

74

Pernyataan sepenuhnya sesuai dalam kode C ++. Pengecualian dan mekanisme penanganan kesalahan lainnya tidak benar-benar dimaksudkan untuk hal yang sama seperti pernyataan.

Penanganan kesalahan adalah ketika ada potensi untuk memulihkan atau melaporkan kesalahan dengan baik kepada pengguna. Misalnya jika ada kesalahan saat mencoba membaca file input, Anda mungkin ingin melakukan sesuatu tentang itu. Kesalahan bisa disebabkan oleh bug, tetapi bisa juga hanya menjadi keluaran yang sesuai untuk masukan yang diberikan.

Pernyataan adalah untuk hal-hal seperti memeriksa bahwa persyaratan API terpenuhi ketika API biasanya tidak diperiksa, atau untuk memeriksa hal-hal yang menurut pengembang dijamin oleh konstruksi. Misalnya, jika suatu algoritme memerlukan masukan yang diurutkan, Anda biasanya tidak akan memeriksanya, tetapi Anda mungkin memiliki pernyataan untuk memeriksanya sehingga debug membangun menandai bug semacam itu. Pernyataan harus selalu menunjukkan program operasi yang salah.


Jika Anda sedang menulis program di mana pematian yang tidak bersih dapat menyebabkan masalah, Anda mungkin ingin menghindari pernyataan. Perilaku tidak terdefinisi secara ketat dalam istilah bahasa C ++ tidak termasuk sebagai masalah di sini, karena membuat pernyataan mungkin sudah merupakan hasil dari perilaku tidak terdefinisi, atau pelanggaran beberapa persyaratan lain yang dapat mencegah beberapa pembersihan berfungsi dengan benar.

Selain itu, jika Anda menerapkan pernyataan dalam istilah pengecualian, maka pernyataan itu berpotensi ditangkap dan 'ditangani' meskipun hal ini bertentangan dengan tujuan pernyataan tersebut.

bames53
sumber
1
Saya tidak sepenuhnya yakin apakah ini dinyatakan secara khusus dalam jawaban, jadi saya akan menyatakannya di sini: Anda tidak boleh menggunakan pernyataan untuk apa pun yang melibatkan input pengguna yang tidak dapat ditentukan pada saat menulis kode. Jika pengguna meneruskan 3alih-alih 1ke kode Anda, secara umum itu tidak akan memicu pernyataan. Pernyataan hanya kesalahan programmer, bukan kesalahan pengguna perpustakaan atau aplikasi.
SS Anne
101
  • Pernyataan ditujukan untuk debugging . Pengguna kode yang Anda kirimkan seharusnya tidak pernah melihatnya. Jika pernyataan dipukul, kode Anda harus diperbaiki.

    CWE-617: Reachable Assertion

Produk berisi pernyataan assert () atau serupa yang dapat dipicu oleh penyerang, yang mengarah ke keluarnya aplikasi atau perilaku lain yang lebih parah dari yang diperlukan.

Meskipun assertion bagus untuk menangkap kesalahan logika dan mengurangi kemungkinan mencapai kondisi kerentanan yang lebih serius, pernyataan itu masih dapat menyebabkan penolakan layanan.

Misalnya, jika server menangani beberapa koneksi simultan, dan assert () terjadi di satu koneksi tunggal yang menyebabkan semua koneksi lainnya terputus, ini adalah pernyataan yang dapat dijangkau yang mengarah ke penolakan layanan.

  • Pengecualian berlaku untuk keadaan luar biasa . Jika ditemukan, pengguna tidak akan dapat melakukan apa yang dia inginkan, tetapi mungkin dapat melanjutkan di tempat lain.

  • Penanganan kesalahan adalah untuk aliran program normal. Misalnya, jika Anda meminta nomor pengguna dan mendapatkan sesuatu yang tidak dapat diuraikan, itu normal , karena input pengguna tidak di bawah kendali Anda dan Anda harus selalu menangani semua kemungkinan situasi sebagai hal yang biasa. (Misalnya, putar ulang hingga Anda memiliki masukan yang valid, dengan mengatakan "Maaf, coba lagi" di antaranya.)

Kerrek SB
sumber
1
datang mencari ini menegaskan kembali; segala bentuk pernyataan yang melewati kode produksi menunjukkan desain dan QA yang buruk. Titik di mana sebuah pernyataan dipanggil adalah tempat penanganan kondisi kesalahan yang baik. (Saya tidak pernah menggunakan assert's). Adapun pengecualian, satu - satunya kasus penggunaan yang saya ketahui adalah ketika ctor mungkin gagal, yang lainnya adalah untuk penanganan kesalahan normal.
slashmais
5
@slashmais: Sentimennya patut dipuji, tetapi kecuali Anda mengirimkan kode bebas bug yang sempurna, saya menemukan pernyataan (bahkan yang membuat pengguna crash) lebih baik daripada perilaku yang tidak ditentukan. Bug terjadi dalam sistem yang kompleks, dan dengan penegasan Anda memiliki cara untuk melihat dan mendiagnosisnya di tempat terjadinya.
Kerrek SB
@KerrekSB Saya lebih suka menggunakan pengecualian atas pernyataan. Setidaknya kode memiliki kesempatan untuk membuang cabang yang gagal dan melakukan hal lain yang berguna. Paling tidak, jika Anda menggunakan RAII, semua buffer Anda untuk membuka file akan dikosongkan dengan benar.
daemonspring
14

Assertion dapat digunakan untuk memverifikasi invarian implementasi internal, seperti status internal sebelum atau setelah eksekusi beberapa metode, dll. Jika pernyataan gagal, ini berarti logika program rusak dan Anda tidak dapat memulihkannya. Dalam hal ini, hal terbaik yang dapat Anda lakukan adalah menghentikannya sesegera mungkin tanpa memberikan pengecualian kepada pengguna. Apa yang benar-benar bagus tentang pernyataan (setidaknya di Linux) adalah bahwa dump inti dihasilkan sebagai hasil dari penghentian proses dan dengan demikian Anda dapat dengan mudah menyelidiki pelacakan tumpukan dan variabel. Ini jauh lebih berguna untuk memahami kegagalan logika daripada pesan pengecualian.

nogard
sumber
Saya memiliki pendekatan serupa. Saya menggunakan pernyataan untuk logika yang mungkin harus benar secara lokal (misalnya invarian loop). Pengecualian adalah ketika kesalahan logika dipaksakan pada kode oleh situasi nonlokal (eksternal).
spraff
Jika pernyataan gagal, itu berarti logika bagian program rusak. Penegasan yang gagal tidak selalu berarti bahwa tidak ada yang bisa diselesaikan. Plugin yang rusak mungkin tidak boleh membatalkan seluruh pengolah kata.
daemonspring
13

Tidak menjalankan destruktor karena alling abort () bukanlah perilaku yang tidak ditentukan!

Jika ya, maka akan menjadi perilaku yang tidak terdefinisi untuk memanggil std::terminate()juga, jadi apa gunanya menyediakannya?

assert() berguna di C ++ seperti di C. Assertion bukan untuk penanganan error, melainkan untuk membatalkan program dengan segera.

Jonathan Wakely
sumber
1
Saya akan mengatakan abort()untuk membatalkan program segera. Anda benar bahwa assertion bukan untuk penanganan error, namun assert mencoba menangani error tersebut dengan membatalkan. Tidakkah seharusnya Anda membuat pengecualian dan membiarkan pemanggil menangani kesalahan tersebut jika bisa? Bagaimanapun, pemanggil berada dalam posisi yang lebih baik untuk menentukan apakah kegagalan satu fungsi membuatnya tidak layak untuk melakukan hal lain. Mungkin si penelepon mencoba melakukan tiga hal yang tidak berhubungan dan masih bisa menyelesaikan dua pekerjaan lainnya dan membuang yang satu ini.
daemonspring
Dan assertdidefinisikan untuk memanggil abort(bila kondisinya salah). Adapun untuk melempar pengecualian, tidak, itu tidak selalu tepat. Beberapa hal tidak dapat ditangani oleh penelepon. Penelepon tidak dapat menentukan apakah bug logika dalam fungsi perpustakaan pihak ketiga dapat dipulihkan, atau jika data yang rusak dapat diperbaiki.
Jonathan Wakely
6

IMHO, pernyataan untuk memeriksa kondisi yang jika dilanggar, membuat segala sesuatu menjadi tidak masuk akal. Dan oleh karena itu Anda tidak dapat pulih dari mereka atau lebih tepatnya, pemulihan tidak relevan.

Saya akan mengelompokkannya menjadi 2 kategori:

  • Dosa pengembang (misalnya fungsi probabilitas yang mengembalikan nilai negatif):

probabilitas float () {return -1.0; }

menegaskan (probabilitas ()> = 0,0)

  • Mesin rusak (mis. Mesin yang menjalankan program Anda sangat salah):

int x = 1;

menegaskan (x> 0);

Keduanya adalah contoh yang sepele tetapi tidak terlalu jauh dari kenyataan. Misalnya, pikirkan tentang algoritme naif yang mengembalikan indeks negatif untuk digunakan dengan vektor. Atau program tertanam di perangkat keras khusus. Atau lebih tepatnya karena omong kosong terjadi .

Dan jika ada kesalahan pengembangan seperti itu, Anda tidak boleh yakin tentang mekanisme pemulihan atau penanganan kesalahan yang diterapkan. Hal yang sama berlaku untuk kesalahan perangkat keras.

FranMowinckel
sumber
1
menegaskan (probabilitas ()> = 0,0)
Elliott