Apakah masuk akal untuk menjaga setiap pointer dereferensi nol?


65

Di pekerjaan baru, saya telah ditandai dalam ulasan kode untuk kode seperti ini:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender) { }

void PowerManager::SignalShutdown()
{
    msgSender_->sendMsg("shutdown()");
}

Saya diberitahu bahwa metode terakhir harus membaca:

void PowerManager::SignalShutdown()
{
    if (msgSender_) {
        msgSender_->sendMsg("shutdown()");
    }
}

yaitu, saya harus menempatkan NULLpenjaga di sekitar msgSender_variabel, meskipun anggota data yang pribadi. Sulit bagi saya untuk menahan diri dari menggunakan kata-kata kasar untuk menggambarkan bagaimana perasaan saya tentang 'kebijaksanaan' ini. Ketika saya meminta penjelasan, saya mendapatkan serangkaian cerita horor tentang bagaimana beberapa programmer junior, beberapa tahun, bingung tentang bagaimana kelas seharusnya bekerja dan secara tidak sengaja menghapus anggota yang tidak seharusnya (dan mengaturnya NULLsetelah itu , tampaknya), dan banyak hal meledak di lapangan tepat setelah rilis produk, dan kami telah "belajar dengan cara yang sulit, percayalah pada kami" bahwa lebih baik hanya NULLmemeriksa semuanya .

Bagi saya, ini terasa seperti pemrograman kultus kargo , sederhana dan sederhana. Beberapa kolega yang bermaksud baik sungguh-sungguh berusaha membantu saya 'mendapatkannya' dan melihat bagaimana ini akan membantu saya menulis kode yang lebih kuat, tetapi ... Saya tidak dapat menahan perasaan bahwa merekalah yang tidak mendapatkannya. .

Apakah masuk akal jika standar pengkodean mengharuskan setiap penunjuk tunggal yang direferensikan dalam suatu fungsi diperiksa NULLterlebih dahulu — bahkan anggota data pribadi? (Catatan: Untuk memberikan beberapa konteks, kami membuat perangkat elektronik konsumen, bukan sistem kontrol lalu lintas udara atau produk 'kegagalan-sama dengan orang-mati' lainnya.)

EDIT : Pada contoh di atas, msgSender_kolaborator bukan opsional. Jika pernah NULL, ini menunjukkan bug. Satu-satunya alasan ia dilewatkan ke konstruktor adalah agar PowerManagerdapat diuji dengan IMsgSendersubkelas tiruan .

RINGKASAN : Ada beberapa jawaban yang sangat bagus untuk pertanyaan ini, terima kasih semuanya. Saya menerima yang dari @ aaronps terutama karena singkatnya. Tampaknya ada kesepakatan umum yang cukup luas bahwa:

  1. Mandat NULLpenjaga untuk setiap pointer dereferenced tunggal adalah berlebihan, tetapi
  2. Anda dapat memihak keseluruhan debat dengan menggunakan referensi sebagai gantinya (jika mungkin) atau sebuah constpointer, dan
  3. assertpernyataan adalah alternatif yang lebih tercerahkan bagi NULLpenjaga untuk memverifikasi bahwa prasyarat fungsi terpenuhi.

14
Jangan berpikir sejenak bahwa kesalahan adalah domain dari programmer junior. 95% pengembang dari semua tingkat pengalaman melakukan sesuatu sesekali. 5% sisanya berbaring melalui gigi mereka.
Blrfl

56
Jika saya melihat seseorang menulis kode yang memeriksa null dan gagal gagal, saya ingin memecat mereka di tempat.
Winston Ewert

16
Hal-hal yang gagal diam-diam adalah yang terburuk. Setidaknya ketika meledak Anda tahu di mana ledakan itu terjadi. Memeriksa nulldan tidak melakukan apa-apa hanyalah cara untuk memindahkan bug ke bawah eksekusi, sehingga jauh lebih sulit untuk melacak kembali ke sumbernya.
MirroredFate

1
+1, pertanyaan yang sangat menarik. Selain jawaban saya, saya juga menunjukkan bahwa ketika Anda harus menulis kode yang tujuannya adalah untuk mengakomodasi unit test, yang sebenarnya Anda lakukan adalah memperdagangkan risiko yang meningkat untuk rasa aman yang salah (lebih banyak baris kode = lebih banyak kemungkinan untuk bug). Mendesain ulang untuk menggunakan referensi tidak hanya meningkatkan keterbacaan kode, tetapi juga mengurangi jumlah kode (dan tes) yang harus Anda tulis untuk membuktikan bahwa kode itu berfungsi.
Seth

6
@Rig, saya yakin ada kasus yang sesuai untuk gagal diam. Tetapi jika seseorang diam gagal sebagai praktik umum (kecuali bekerja di ranah yang masuk akal), saya benar-benar tidak ingin mengerjakan proyek yang mereka letakkan kodenya.
Winston Ewert

Jawaban:


66

Itu tergantung pada 'kontrak':

Jika PowerManager HARUS memiliki yang valid IMsgSender, jangan pernah memeriksa nol, biarkan lebih cepat mati.

Jika di sisi lain, itu MUNGKIN memiliki IMsgSender, maka Anda perlu memeriksa setiap kali Anda menggunakan, sesederhana itu.

Komentar akhir tentang kisah programmer junior, masalahnya sebenarnya adalah kurangnya prosedur pengujian.


4
Memberi +1 untuk jawaban yang sangat ringkas yang meringkas seperti yang saya rasakan: "itu tergantung ..." Anda juga punya nyali untuk mengatakan "tidak pernah memeriksa nol" ( jika nol tidak valid). Menempatkan assert()dalam setiap fungsi anggota yang menunjuk pointer adalah kompromi yang kemungkinan akan menenangkan banyak orang, tetapi bahkan itu terasa seperti 'paranoia spekulatif' bagi saya. Daftar hal-hal di luar kemampuan saya untuk mengendalikan tidak terbatas, dan begitu saya membiarkan diri saya mulai mengkhawatirkannya, itu menjadi lereng yang sangat licin. Belajar mempercayai pengujian saya, kolega saya — dan debugger saya — telah membantu saya tidur lebih nyenyak di malam hari.
evadeflow

2
Saat Anda membaca kode, pernyataan itu bisa sangat membantu dalam memahami bagaimana kode seharusnya bekerja. Saya tidak pernah menemukan basis kode yang membuat saya pergi, 'Saya berharap itu tidak memiliki semua pernyataan ini di semua tempat.', Tapi saya telah melihat banyak di mana saya mengatakan sebaliknya.
Kristof Provost

1
Sederhana dan sederhana, ini adalah jawaban yang benar untuk pertanyaan itu.
Matthew Azkimov

2
Ini adalah jawaban benar yang paling dekat tetapi saya tidak bisa setuju dengan 'tidak pernah memeriksa nol', selalu periksa parameter tidak valid pada titik mereka akan ditugaskan ke variabel anggota.
James

Terima kasih atas komentarnya. Lebih baik membiarkannya crash lebih cepat jika seseorang tidak membaca spesifikasi dan menginisialisasi dengan nilai nol ketika harus memiliki sesuatu yang valid di sana.
aaronps

77

Saya merasa kode harus dibaca:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}

Ini sebenarnya lebih baik daripada menjaga NULL, karena membuatnya sangat jelas bahwa fungsi tidak boleh dipanggil jika msgSender_NULL. Ini juga memastikan bahwa Anda akan melihat jika ini terjadi.

"Kebijaksanaan" yang dibagikan dari kolega Anda akan dengan diam-diam mengabaikan kesalahan ini, dengan hasil yang tidak terduga.

Secara umum bug lebih mudah diperbaiki jika terdeteksi lebih dekat ke penyebabnya. Dalam contoh ini, penjaga NULL yang diusulkan akan menyebabkan pesan penutupan tidak disetel, yang mungkin atau mungkin tidak menghasilkan bug yang terlihat. Anda akan mengalami kesulitan bekerja mundur ke SignalShutdownfungsi daripada jika seluruh aplikasi baru saja mati, menghasilkan backtrace berguna atau keren yang menunjuk langsung ke SignalShutdown().

Ini sedikit berlawanan dengan intuisi, tetapi menabrak begitu ada sesuatu yang salah cenderung membuat kode Anda lebih kuat. Itu karena Anda benar-benar menemukan masalahnya, dan cenderung memiliki penyebab yang sangat jelas juga.


10
Perbedaan besar antara panggilan assert dan contoh kode OP adalah bahwa assert hanya diaktifkan di debug builds (#define NDEBUG) Ketika produk masuk ke tangan pelanggan, dan debug dinonaktifkan, dan kombinasi jalur kode yang tidak pernah ditelusuri dijalankan untuk meninggalkan pointer null meskipun fungsi tersebut dijalankan, pernyataan tersebut tidak memberikan Anda perlindungan.
atk

17
Mungkin Anda sudah menguji coba bangunan sebelum benar-benar mengirimkannya ke pelanggan, tetapi ya, ini adalah risiko yang mungkin. Namun, solusinya mudah: hanya kirimkan dengan konfirmasi diaktifkan (itu adalah versi yang Anda uji tetap) atau ganti dengan makro Anda sendiri yang menegaskan dalam mode debug dan hanya log, log + backtraces, keluar, ... dalam mode rilis.
Kristof Provost

7
@ James - dalam 90% kasus Anda tidak akan pulih dari cek NULL yang gagal juga. Tetapi dalam kasus seperti itu, kode akan secara diam-diam gagal dan kesalahan mungkin muncul jauh di kemudian hari - misalnya Anda berpikir bahwa Anda menyimpan ke file tetapi sebenarnya pemeriksaan nol terkadang gagal membuat Anda tidak bijak. Anda tidak dapat pulih dari semua situasi yang tidak boleh terjadi. Anda dapat memverifikasi bahwa itu tidak akan terjadi, tetapi saya tidak berpikir Anda memiliki anggaran kecuali Anda menulis kode untuk satelit NASA atau pembangkit listrik tenaga nuklir. Anda perlu memiliki cara untuk mengatakan "Saya tidak tahu apa yang terjadi - program ini tidak seharusnya berada dalam keadaan ini".
Maciej Piechotka

6
@ James, saya tidak setuju dengan lemparan. Itu membuatnya terlihat seperti sesuatu yang benar-benar bisa terjadi. Menegaskan adalah untuk hal-hal yang seharusnya tidak mungkin. Untuk bug nyata dalam kode dengan kata lain. Pengecualian berlaku ketika hal-hal yang tidak terduga, tetapi mungkin terjadi, terjadi. Pengecualian adalah untuk hal-hal yang bisa Anda tangani untuk menangani dan memulihkannya. Kesalahan logika dalam kode yang tidak dapat Anda pulihkan, dan Anda juga tidak boleh mencoba.
Kristof Provost

2
@ James: Dalam pengalaman saya dengan sistem embedded, perangkat lunak dan perangkat keras dirancang sedemikian rupa sehingga sangat sulit untuk membuat perangkat yang sama sekali tidak dapat digunakan. Dalam sebagian besar kasus, ada pengawas perangkat keras yang memaksa reset perangkat sepenuhnya jika perangkat lunak berhenti berjalan.
Bart van Ingen Schenau

30

Jika msgSender tidak boleh null, Anda harus meletakkan nullcek hanya di konstruktor. Ini juga berlaku untuk input lain ke dalam kelas - masukkan pemeriksaan integritas pada titik masuk ke 'modul' - kelas, fungsi dll.

Aturan praktis saya adalah melakukan pemeriksaan integritas antara batas-batas modul - kelas dalam hal ini. Selain itu, sebuah kelas harus cukup kecil sehingga memungkinkan untuk secara cepat memverifikasi integritas masa hidup anggota kelas - memastikan bahwa kesalahan seperti penghapusan / penghapusan yang tidak tepat dapat dicegah. Pemeriksaan nol yang dilakukan dalam kode di pos Anda mengasumsikan bahwa setiap penggunaan yang tidak valid benar-benar memberikan nol pada penunjuk - yang tidak selalu demikian. Karena 'penggunaan yang tidak valid' secara inheren menyiratkan bahwa asumsi tentang kode reguler tidak berlaku, kami tidak dapat memastikan untuk menangkap semua jenis kesalahan pointer - misalnya penghapusan, kenaikan yang tidak valid dll.

Selain itu - jika Anda yakin argumennya tidak akan pernah batal, pertimbangkan untuk menggunakan referensi, tergantung pada penggunaan kelas Anda. Kalau tidak, pertimbangkan untuk menggunakan std::unique_ptratau std::shared_ptrsebagai pengganti pointer mentah.


11

Tidak, itu tidak masuk akal untuk memeriksa masing-masing dan setiap pointer pointer untuk pointer yang sedang NULL.

Pemeriksaan null-pointer bernilai pada argumen fungsi (termasuk argumen konstruktor) untuk memastikan prasyarat terpenuhi atau untuk mengambil tindakan yang sesuai jika parameter opsional tidak disediakan, dan mereka berharga untuk memeriksa invarian kelas setelah Anda membuka internal kelas. Tetapi jika satu-satunya alasan pointer menjadi NULL adalah adanya bug, maka tidak ada gunanya memeriksa. Bug itu bisa dengan mudah mengatur pointer ke nilai yang tidak valid lainnya.

Jika saya dihadapkan pada situasi seperti Anda, saya akan mengajukan dua pertanyaan:

  • Mengapa kesalahan dari programmer junior itu tidak ketahuan sebelum rilis? Misalnya dalam ulasan kode atau dalam fase pengujian? Membawa perangkat elektronik konsumen yang salah ke pasar bisa sama berbiaya (jika Anda memperhitungkan pangsa pasar dan niat baik) dengan melepaskan perangkat yang salah yang digunakan dalam aplikasi yang kritis terhadap keselamatan, jadi saya berharap perusahaan akan serius dalam pengujian dan kegiatan QA lainnya.
  • Jika null-check gagal, penanganan kesalahan apa yang Anda harapkan dari saya, atau bisakah saya menulis assert(msgSender_)alih-alih cek nol? Jika Anda hanya memasukkan null check-in, Anda mungkin telah mencegah crash, tetapi Anda mungkin telah menciptakan situasi yang lebih buruk karena perangkat lunak berlanjut dengan premis bahwa suatu operasi telah terjadi sementara pada kenyataannya operasi itu dilewati. Ini dapat menyebabkan bagian lain dari perangkat lunak menjadi tidak stabil.

9

Contoh ini tampaknya lebih tentang umur objek daripada apakah atau tidak parameter input nol †. Karena Anda menyebutkan bahwa PowerManagerharus selalu memiliki valid IMsgSender, melewati argumen dengan pointer (dengan demikian memungkinkan kemungkinan pointer nol) menurut saya sebagai cacat desain ††.

Dalam situasi seperti ini, saya lebih suka mengubah antarmuka sehingga persyaratan pemanggil diberlakukan oleh bahasa:

PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}

Menulis ulang dengan cara ini mengatakan bahwa PowerManagerperlu menyimpan referensi IMsgSenderuntuk seumur hidup itu. Ini, pada gilirannya, juga menetapkan persyaratan tersirat yang IMsgSenderharus hidup lebih lama dari PowerManager, dan meniadakan kebutuhan untuk setiap pemeriksaan penunjuk nol atau pernyataan di dalam PowerManager.

Anda juga dapat menulis hal yang sama menggunakan pointer pintar (via boost atau c ++ 11), untuk secara paksa memaksa IMsgSenderhidup lebih lama dari PowerManager:

PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}

Metode ini lebih disukai jika ada kemungkinan IMsgSenderseumur hidup tidak dapat dijamin lebih lama dari PowerManageritu (yaitu, x = new IMsgSender(); p = new PowerManager(*x);).

† Sehubungan dengan pointer: pengecekan null yang merajalela membuat kode lebih sulit dibaca, dan tidak meningkatkan stabilitas (itu meningkatkan penampilan stabilitas, yang jauh lebih buruk).

Di suatu tempat, seseorang mendapat alamat untuk menyimpan memori IMsgSender. Adalah tanggung jawab fungsi tersebut untuk memastikan bahwa alokasi berhasil (memeriksa nilai pengembalian perpustakaan atau menangani std::bad_allocpengecualian dengan benar ), agar tidak melewati pointer yang tidak valid.

Karena PowerManagertidak memiliki IMsgSender(itu hanya meminjamnya untuk sementara waktu), itu tidak bertanggung jawab untuk mengalokasikan atau menghancurkan memori itu. Ini adalah alasan lain mengapa saya lebih suka referensi.

†† Karena Anda baru di pekerjaan ini, saya berharap Anda meretas kode yang ada. Jadi dengan cacat desain, maksud saya cacat itu ada dalam kode yang Anda kerjakan. Jadi, orang-orang yang menandai kode Anda karena tidak memeriksa pointer nol benar-benar menandai diri mereka sendiri untuk menulis kode yang memerlukan pointer :)


1
Sebenarnya tidak ada yang salah dengan menggunakan pointer mentah kadang-kadang. Std :: shared_ptr bukan peluru perak dan menggunakannya dalam daftar argumen adalah obat untuk teknik ceroboh meskipun saya menyadari itu dianggap 'leet' untuk menggunakannya kapan pun memungkinkan. Gunakan java jika Anda merasa seperti itu.
James

2
Saya tidak mengatakan pointer mentah itu buruk! Mereka pasti memiliki tempat mereka. Namun, ini adalah C + + , referensi (dan berbagi pointer, sekarang) adalah bagian dari bahasa karena suatu alasan. Gunakan mereka, mereka akan membuat Anda berhasil.
Seth

Saya suka ide smart pointer, tapi itu bukan pilihan di pertunjukan khusus ini. +1 untuk merekomendasikan penggunaan referensi (seperti @Max dan beberapa lainnya miliki). Kadang-kadang tidak mungkin untuk menyuntikkan referensi, meskipun, karena masalah ketergantungan ayam dan telur, yaitu, jika suatu objek yang seharusnya menjadi kandidat untuk injeksi perlu memiliki pointer (atau referensi) ke pemegangnya yang disuntikkan ke dalamnya . Ini kadang-kadang dapat menunjukkan desain yang sangat erat; tetapi sering diterima, seperti dalam hubungan antara objek dan iteratornya, di mana tidak pernah masuk akal untuk menggunakan yang terakhir tanpa yang pertama.
evadeflow

8

Seperti halnya pengecualian, kondisi penjaga hanya berguna jika Anda tahu apa yang harus dilakukan untuk memulihkan dari kesalahan atau jika Anda ingin memberikan pesan pengecualian yang lebih bermakna.

Menelan kesalahan (apakah tertangkap sebagai pengecualian atau pemeriksaan penjaga), hanya hal yang tepat untuk dilakukan ketika kesalahan tidak masalah. Tempat paling umum bagi saya untuk melihat kesalahan yang ditelan adalah dalam kode pencatatan kesalahan - Anda tidak ingin merusak aplikasi karena Anda tidak dapat mencatat pesan status.

Kapan saja suatu fungsi dipanggil, dan itu bukan perilaku opsional, itu harus gagal keras, tidak diam-diam.

Sunting: saat memikirkan kisah programmer junior Anda, kedengarannya apa yang terjadi adalah bahwa seorang anggota pribadi ditetapkan untuk batal ketika hal itu seharusnya tidak pernah diizinkan terjadi. Mereka memiliki masalah dengan penulisan yang tidak pantas dan berusaha memperbaikinya dengan memvalidasi setelah membaca. Ini mundur. Pada saat Anda mengidentifikasi itu, kesalahan sudah terjadi. Solusi yang diberlakukan kode-review / kompiler untuk itu bukan kondisi penjaga, tetapi sebaliknya getter dan setter atau anggota const.


7

Seperti yang telah dicatat oleh orang lain, ini tergantung pada apakah atau tidak msgSenderdapat secara sah NULL . Berikut ini mengasumsikan bahwa itu tidak boleh NULL.

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

"Perbaikan" yang diusulkan oleh orang lain di tim Anda melanggar prinsip Dead Programs Tell No Lies . Bug sangat sulit ditemukan. Metode yang mengubah perilakunya secara diam-diam berdasarkan masalah sebelumnya, tidak hanya membuatnya sulit untuk menemukan bug pertama tetapi juga menambahkan bug ke-2 sendiri.

Si junior mendatangkan malapetaka dengan tidak memeriksa nol. Bagaimana jika potongan kode ini mendatangkan malapetaka dengan terus berjalan dalam keadaan tidak terdefinisi (perangkat hidup tetapi program "berpikir" tidak aktif)? Mungkin bagian lain dari program akan melakukan sesuatu yang hanya aman ketika perangkat mati.

Salah satu dari pendekatan ini akan menghindari kegagalan diam:

  1. Gunakan pernyataan seperti yang disarankan oleh jawaban ini , tetapi pastikan mereka diaktifkan dalam kode produksi. Ini, tentu saja, dapat menyebabkan masalah jika pernyataan lain ditulis dengan asumsi bahwa mereka tidak aktif dalam produksi.

  2. Lempar pengecualian jika itu nol.


5

Saya setuju dengan menjebak null di konstruktor. Lebih lanjut, jika anggota dinyatakan dalam tajuk sebagai:

IMsgSender* const msgSender_;

Maka pointer tidak dapat diubah setelah inisialisasi, jadi jika itu baik-baik saja pada konstruksi itu akan baik-baik saja untuk seumur hidup objek yang mengandungnya. (Object menunjuk ke akan tidak menjadi const.)


Itu saran yang bagus , terima kasih! Begitu baik, pada kenyataannya, aku minta maaf aku tidak bisa memilihnya lebih tinggi. Ini bukan jawaban langsung untuk pertanyaan yang disebutkan, tetapi ini adalah cara yang bagus untuk menghilangkan kemungkinan penunjuk menjadi 'tidak sengaja' NULL.
evadeflow

@ evadeflow, saya pikir "Saya setuju dengan yang lain" menjawab pertanyaan utama. Ceria! ;-)
Grimm The Opiner

Akan sedikit tidak sopan bagiku untuk mengubah jawaban yang diterima pada titik ini, mengingat cara pertanyaan itu diungkapkan. Tapi saya benar-benar berpikir nasihat ini luar biasa, dan saya minta maaf saya tidak memikirkannya sendiri. Dengan sebuah constpointer, tidak perlu ke assert()luar konstruktor, jadi jawaban ini tampaknya bahkan sedikit lebih baik daripada @Kristof Provost (yang juga luar biasa, tetapi juga menjawab pertanyaan secara tidak langsung.) Saya berharap orang lain akan memilih ini, karena itu benar-benar tidak masuk akal untuk assertsetiap metode ketika Anda bisa membuat pointer const.
evadeflow

@evadeflow Orang hanya mengunjungi Questins untuk rep, sekarang sudah dijawab tidak ada yang akan melihatnya lagi. ;-) Saya hanya muncul karena itu adalah pertanyaan tentang pointer dan saya mengawasi keluar untuk brigade "Gunakan smart pointer untuk semuanya selalu".
Grimm The Opiner

3

Ini Sangat Berbahaya!

Saya bekerja di bawah pengembang senior dalam basis kode C dengan "standar" shoddiest yang mendorong untuk hal yang sama, untuk secara buta memeriksa semua pointer untuk null. Pengembang akhirnya akan melakukan hal-hal seperti ini:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

Saya pernah mencoba menghapus centang prasyarat seperti itu satu kali dalam fungsi seperti itu dan menggantinya dengan assertuntuk melihat apa yang akan terjadi.

Betapa terkejutnya saya, saya menemukan ribuan baris kode dalam basis kode yang meneruskan nol ke fungsi ini, tetapi di mana pengembang, kemungkinan bingung, bekerja di sekitar dan hanya menambahkan lebih banyak kode sampai semuanya berjalan.

Untuk ngeri saya lebih lanjut, saya menemukan masalah ini lazim di semua tempat di basis kode memeriksa nol. Basis kode telah tumbuh selama beberapa dekade untuk mengandalkan cek ini untuk dapat secara diam-diam melanggar bahkan prasyarat yang paling jelas didokumentasikan. Dengan menghapus cek mematikan ini demi asserts, semua kesalahan manusia logis selama beberapa dekade dalam basis kode akan terungkap, dan kami akan tenggelam di dalamnya.

Hanya butuh dua baris kode yang tampaknya tidak bersalah seperti waktu ini + dan sebuah tim untuk akhirnya menutupi ribuan bug yang terakumulasi.

Ini adalah jenis praktik yang membuat bug bergantung pada bug lain agar ada agar perangkat lunak berfungsi. Ini adalah skenario mimpi buruk . Itu juga membuat setiap kesalahan logis terkait dengan melanggar prasyarat seperti itu muncul secara misterius sejuta baris kode dari situs yang sebenarnya di mana kesalahan terjadi, karena semua cek nol ini hanya menyembunyikan bug dan menyembunyikan bug sampai kami mencapai tempat yang lupa untuk menyembunyikan bug.

Untuk hanya memeriksa nulls secara membabi buta di setiap tempat di mana pointer nol melanggar suatu prasyarat, bagi saya, benar-benar gila, kecuali perangkat lunak Anda sangat kritis terhadap kegagalan pernyataan dan produksi macet sehingga potensi skenario ini lebih disukai.

Apakah masuk akal jika standar pengkodean mengharuskan setiap penunjuk tunggal yang direferensikan dalam suatu fungsi diperiksa untuk NULL terlebih dahulu — bahkan anggota data pribadi?

Jadi saya katakan, sama sekali tidak. Itu bahkan tidak "aman". Mungkin justru sebaliknya dan menutupi semua jenis bug di seluruh basis kode Anda yang, selama bertahun-tahun, dapat menyebabkan skenario yang paling mengerikan.

assertadalah cara untuk pergi ke sini. Pelanggaran atas prasyarat tidak boleh dibiarkan begitu saja, atau hukum Murphy bisa dengan mudah dikeluarkan.


1
"menegaskan" adalah cara untuk pergi - tetapi ingat bahwa pada sistem modern, setiap akses memori melalui pointer memiliki pernyataan null-pointer yang dibangun ke dalam perangkat keras. Jadi dalam "menegaskan (p! = NULL); return * p;" bahkan pernyataan itu sebagian besar tidak ada gunanya.
gnasher729

@ gnasher729 Ah, itu poin yang sangat bagus, tapi saya cenderung melihat assertkeduanya sebagai mekanisme dokumentasi untuk menetapkan apa prasyarat itu dan bukan hanya cara untuk memaksa membatalkan. Tetapi saya juga menyukai sifat dari pernyataan kegagalan yang menunjukkan kepada Anda baris kode mana yang menyebabkan kegagalan dan kondisi pernyataan gagal ("mendokumentasikan kecelakaan"). Dapat berguna jika kita akhirnya menggunakan build debug di luar debugger dan tertangkap basah.

@ gnasher729 Atau saya mungkin salah paham bagian dari itu. Apakah sistem modern ini menunjukkan baris kode sumber mana yang pernyataannya gagal? Saya biasanya membayangkan mereka kehilangan info pada saat itu dan mungkin hanya menunjukkan pelanggaran segfault / akses, tapi saya agak jauh dari "modern" - masih keras kepala di Windows 7 untuk sebagian besar perkembangan saya.

Misalnya, pada MacOS X dan iOS, jika aplikasi Anda macet karena akses pointer nol selama pengembangan, debugger akan memberi tahu Anda baris kode yang tepat di mana itu terjadi. Ada aspek aneh lainnya: Kompiler akan mencoba memberi Anda peringatan jika Anda melakukan sesuatu yang mencurigakan. Jika Anda melewatkan sebuah pointer ke suatu fungsi dan mengaksesnya, kompiler tidak akan memberi Anda peringatan bahwa pointer tersebut mungkin NULL, karena itu sering terjadi, Anda akan tenggelam dalam peringatan. Namun, jika Anda melakukan perbandingan if (p == NULL) ... maka kompiler mengasumsikan bahwa ada peluang bagus bahwa p adalah NULL,
gnasher729

karena mengapa Anda harus mengujinya sebaliknya, karena itu akan memberikan peringatan sejak saat itu jika Anda menggunakannya tanpa pengujian. Jika Anda menggunakan makro Anda sendiri seperti-tegas, Anda harus sedikit pintar untuk menulisnya sehingga "my_assert (p! = NULL," Ini adalah pointer nol, bodoh! "); * P = 1;" tidak memberi Anda peringatan.
gnasher729

2

Objective-C , misalnya, memperlakukan setiap pemanggilan metode pada nilobjek sebagai no-op yang mengevaluasi ke nilai nol-ish. Ada beberapa keuntungan dari keputusan desain di Objective-C, untuk alasan yang disarankan dalam pertanyaan Anda. Konsep teoritis dari null-guarding setiap pemanggilan metode memiliki beberapa kelebihan jika dipublikasikan dengan baik dan diterapkan secara konsisten.

Yang mengatakan, kode hidup dalam suatu ekosistem, bukan ruang hampa. Perilaku null-guarded akan non-idiomatis dan mengejutkan dalam C ++, dan karenanya harus dianggap berbahaya. Singkatnya, tidak ada yang menulis kode C ++ seperti itu, jadi jangan lakukan itu! Namun, sebagai contoh tandingan, catat bahwa panggilan free()atau deletea NULLdalam C dan C ++ dijamin menjadi no-op.


Dalam contoh Anda, mungkin akan bermanfaat untuk menempatkan pernyataan di konstruktor yang msgSenderbukan nol. Jika konstruktor disebut metode pada msgSenderlangsung, maka tidak ada pernyataan seperti itu akan diperlukan, karena akan crash di sana pula. Namun, karena ini hanya menyimpan msgSender untuk penggunaan di masa mendatang, tidak akan terlihat jelas dari melihat jejak tumpukan tentang SignalShutdown()bagaimana nilai itu muncul NULL, sehingga pernyataan dalam konstruktor akan membuat proses debug secara signifikan lebih mudah.

Lebih baik lagi, konstruktor harus menerima const IMsgSender&referensi, yang tidak mungkin NULL.


1

Alasan mengapa Anda diminta untuk menghindari null dereferences adalah untuk memastikan bahwa kode Anda kuat. Contoh-contoh programer junior sejak lama hanyalah contoh. Siapa pun dapat memecahkan kode secara tidak sengaja dan menyebabkan dereferensi nol - terutama untuk global dan global kelas. Dalam C dan C ++ ini bahkan lebih mungkin terjadi secara tidak sengaja, dengan kemampuan manajemen memori langsung. Anda mungkin terkejut, tetapi hal seperti ini sering terjadi. Bahkan oleh pengembang yang sangat berpengetahuan, sangat berpengalaman, dan sangat senior.

Anda tidak perlu mengecek semuanya, tetapi Anda harus melindungi terhadap dereferensi yang memiliki kemungkinan layak nol. Ini biasanya ketika mereka dialokasikan, digunakan dan disereferensi dalam fungsi yang berbeda. Mungkin saja salah satu fungsi lainnya akan dimodifikasi dan merusak fungsi Anda. Mungkin juga salah satu dari fungsi-fungsi lain dapat dipanggil rusak (seperti jika Anda memiliki deallocator yang dapat dipanggil secara terpisah dari destructor).

Saya lebih suka pendekatan rekan kerja Anda memberitahu Anda dalam kombinasi dengan menggunakan tegas. Kecelakaan di lingkungan pengujian sehingga lebih jelas bahwa ada masalah untuk diperbaiki dan gagal dalam produksi.

Anda juga harus menggunakan alat kebenaran kode yang kuat seperti coverity atau fortify. Dan Anda harus mengatasi semua peringatan kompiler.

Sunting: seperti yang telah disebutkan orang lain, gagal secara diam-diam, seperti dalam beberapa contoh kode, umumnya adalah hal yang salah juga. Jika fungsi Anda tidak dapat pulih dari nilai nol, itu harus mengembalikan kesalahan (atau melempar pengecualian) ke pemanggilnya. Penelepon bertanggung jawab untuk kemudian memperbaiki urutan panggilannya, memulihkan, atau mengembalikan kesalahan (atau melempar pengecualian) ke peneleponnya, dan seterusnya. Akhirnya, salah satu fungsi dapat memulihkan dan melanjutkan dengan anggun, memulihkan dan gagal dengan anggun (seperti database gagal transaksi karena kesalahan internal untuk satu pengguna tetapi tidak keluar secara akut), atau fungsi menentukan bahwa keadaan aplikasi rusak dan tidak dapat dipulihkan dan aplikasi keluar.


+1 karena memiliki keberanian untuk mendukung posisi (yang tampaknya) tidak populer. Saya akan kecewa jika tidak ada yang membela rekan saya dalam hal ini (mereka adalah orang-orang yang sangat pintar yang telah mengeluarkan produk berkualitas selama bertahun-tahun). Saya juga menyukai gagasan bahwa aplikasi harus "macet di lingkungan pengujian ... dan gagal dalam produksi." Saya tidak yakin bahwa mandat NULLpemeriksaan 'hafalan' adalah cara untuk melakukan itu, tetapi saya menghargai mendengar pendapat dari seseorang di luar tempat kerja saya yang pada dasarnya mengatakan: "Aigh. Kelihatannya tidak terlalu tidak masuk akal."
evadeflow

Saya merasa kesal ketika melihat orang menguji NULL setelah malloc (). Ini memberitahu saya bahwa mereka tidak mengerti bagaimana OS modern mengelola memori.
Kristof Provost

2
Dugaan saya adalah bahwa @KristofProvost berbicara tentang OS yang menggunakan overcommit, untuk yang malloc selalu berhasil (tetapi OS nantinya dapat membunuh proses Anda jika sebenarnya tidak ada cukup memori yang tersedia). Namun, ini bukan alasan yang baik untuk melewatkan cek nol di malloc: overcommit tidak universal di seluruh platform dan bahkan jika itu diaktifkan, mungkin ada alasan lain untuk malloc gagal (misalnya, di Linux, batas ruang alamat proses diatur dengan ulimit ).
John Bartholomew

1
Apa kata John. Saya memang berbicara tentang overcommit. Overcommit diaktifkan secara default di Linux (yang membayar tagihan saya). Saya tidak tahu, tetapi curiga begitu, apakah itu sama pada Windows. Dalam sebagian besar kasus, Anda akan berakhir crash, kecuali jika Anda siap untuk menulis banyak kode yang tidak pernah, akan pernah diuji untuk menangani kesalahan yang hampir pasti tidak akan pernah terjadi ...
Kristof Provost

2
Biarkan saya menambahkan bahwa saya juga tidak pernah melihat kode (di luar kernel linux, di mana kehabisan memori benar-benar mungkin terjadi) yang sebenarnya akan benar-benar menangani kondisi kehabisan memori. Semua kode yang pernah saya coba coba akan crash nanti. Semua yang dicapai adalah membuang-buang waktu, membuat kode lebih sulit untuk dipahami dan menyembunyikan masalah sebenarnya. Null dereferences mudah untuk di-debug (jika Anda memiliki file inti).
Kristof Provost

1

Penggunaan pointer bukan referensi akan memberi tahu saya bahwa msgSenderitu hanya opsi dan di sini cek nol akan benar. Cuplikan kode terlalu lambat untuk memutuskan itu. Mungkin ada elemen lain PowerManageryang berharga (atau dapat diuji) ...

Saat memilih antara pointer dan referensi, saya menimbang kedua opsi dengan seksama. Jika saya harus menggunakan pointer untuk anggota (bahkan untuk anggota pribadi), saya harus menerima if (x)prosedur setiap kali saya men-decferencing itu.


Seperti yang saya sebutkan di komentar lain di suatu tempat, ada kalanya menyuntikkan referensi tidak mungkin. Contoh yang sering saya
jumpai

@ evadeflow: OK, saya akui, itu tergantung pada ukuran kelas dan visibilitas anggota pointer (yang tidak dapat dijadikan referensi), apakah saya memeriksa sebelum dereferencing. Dalam kasus di mana kelasnya kecil dan sederhana dan penunjuknya pribadi, saya tidak ...
Wolf

0

Itu tergantung pada apa yang Anda inginkan terjadi.

Anda menulis bahwa Anda sedang mengerjakan "perangkat elektronik konsumen". Jika, entah bagaimana, bug diperkenalkan oleh pengaturan msgSender_ke NULL, apakah Anda mau

  • perangkat untuk melanjutkan, melewatkan SignalShutdowntetapi melanjutkan sisa operasinya, atau
  • perangkat macet, memaksa pengguna untuk menyalakannya kembali?

Bergantung pada dampak sinyal mematikan yang tidak dikirim, opsi 1 mungkin merupakan pilihan yang layak. Jika pengguna dapat terus mendengarkan musiknya, tetapi layar masih menunjukkan judul trek sebelumnya, yang mungkin lebih disukai daripada kerusakan perangkat secara total.

Tentu saja, jika Anda memilih opsi 1, sebuah assert(seperti yang direkomendasikan oleh orang lain) sangat penting untuk mengurangi kemungkinan bug tersebut merayap tanpa diketahui selama pengembangan. Ituif guard nol hanya ada untuk mitigasi kegagalan dalam penggunaan produksi.

Secara pribadi, saya juga lebih suka pendekatan "crash awal" untuk membangun produksi, tetapi saya sedang mengembangkan perangkat lunak bisnis yang dapat diperbaiki dan diperbarui dengan mudah jika ada bug. Untuk perangkat elektronik konsumen, ini mungkin tidak mudah.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.