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 NULL
penjaga 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 NULL
setelah 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 NULL
memeriksa 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 NULL
terlebih 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 PowerManager
dapat diuji dengan IMsgSender
subkelas 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:
- Mandat
NULL
penjaga untuk setiap pointer dereferenced tunggal adalah berlebihan, tetapi - Anda dapat memihak keseluruhan debat dengan menggunakan referensi sebagai gantinya (jika mungkin) atau sebuah
const
pointer, dan assert
pernyataan adalah alternatif yang lebih tercerahkan bagiNULL
penjaga untuk memverifikasi bahwa prasyarat fungsi terpenuhi.
sumber
null
dan tidak melakukan apa-apa hanyalah cara untuk memindahkan bug ke bawah eksekusi, sehingga jauh lebih sulit untuk melacak kembali ke sumbernya.Jawaban:
Itu tergantung pada 'kontrak':
Jika
PowerManager
HARUS memiliki yang validIMsgSender
, 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.
sumber
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.Saya merasa kode harus dibaca:
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
SignalShutdown
fungsi daripada jika seluruh aplikasi baru saja mati, menghasilkan backtrace berguna atau keren yang menunjuk langsung keSignalShutdown()
.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.
sumber
Jika msgSender tidak boleh
null
, Anda harus meletakkannull
cek 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_ptr
ataustd::shared_ptr
sebagai pengganti pointer mentah.sumber
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:
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.sumber
Contoh ini tampaknya lebih tentang umur objek daripada apakah atau tidak parameter input nol †. Karena Anda menyebutkan bahwa
PowerManager
harus selalu memiliki validIMsgSender
, 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:
Menulis ulang dengan cara ini mengatakan bahwa
PowerManager
perlu menyimpan referensiIMsgSender
untuk seumur hidup itu. Ini, pada gilirannya, juga menetapkan persyaratan tersirat yangIMsgSender
harus hidup lebih lama dariPowerManager
, dan meniadakan kebutuhan untuk setiap pemeriksaan penunjuk nol atau pernyataan di dalamPowerManager
.Anda juga dapat menulis hal yang sama menggunakan pointer pintar (via boost atau c ++ 11), untuk secara paksa memaksa
IMsgSender
hidup lebih lama dariPowerManager
:Metode ini lebih disukai jika ada kemungkinan
IMsgSender
seumur hidup tidak dapat dijamin lebih lama dariPowerManager
itu (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 menanganistd::bad_alloc
pengecualian dengan benar ), agar tidak melewati pointer yang tidak valid.Karena
PowerManager
tidak memilikiIMsgSender
(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 :)
sumber
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.
sumber
Seperti yang telah dicatat oleh orang lain, ini tergantung pada apakah atau tidak
msgSender
dapat secara sahNULL
. Berikut ini mengasumsikan bahwa itu tidak boleh NULL."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:
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.
Lempar pengecualian jika itu nol.
sumber
Saya setuju dengan menjebak null di konstruktor. Lebih lanjut, jika anggota dinyatakan dalam tajuk sebagai:
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.)
sumber
NULL
.const
pointer, tidak perlu keassert()
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 untukassert
setiap metode ketika Anda bisa membuat pointerconst
.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:
Saya pernah mencoba menghapus centang prasyarat seperti itu satu kali dalam fungsi seperti itu dan menggantinya dengan
assert
untuk 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.
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.
assert
adalah cara untuk pergi ke sini. Pelanggaran atas prasyarat tidak boleh dibiarkan begitu saja, atau hukum Murphy bisa dengan mudah dikeluarkan.sumber
assert
keduanya 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.Objective-C , misalnya, memperlakukan setiap pemanggilan metode pada
nil
objek 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()
ataudelete
aNULL
dalam C dan C ++ dijamin menjadi no-op.Dalam contoh Anda, mungkin akan bermanfaat untuk menempatkan pernyataan di konstruktor yang
msgSender
bukan nol. Jika konstruktor disebut metode padamsgSender
langsung, maka tidak ada pernyataan seperti itu akan diperlukan, karena akan crash di sana pula. Namun, karena ini hanya menyimpanmsgSender
untuk penggunaan di masa mendatang, tidak akan terlihat jelas dari melihat jejak tumpukan tentangSignalShutdown()
bagaimana nilai itu munculNULL
, sehingga pernyataan dalam konstruktor akan membuat proses debug secara signifikan lebih mudah.Lebih baik lagi, konstruktor harus menerima
const IMsgSender&
referensi, yang tidak mungkinNULL
.sumber
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.
sumber
NULL
pemeriksaan '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."Penggunaan pointer bukan referensi akan memberi tahu saya bahwa
msgSender
itu hanya opsi dan di sini cek nol akan benar. Cuplikan kode terlalu lambat untuk memutuskan itu. Mungkin ada elemen lainPowerManager
yang 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.sumber
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_
keNULL
, apakah Anda mauSignalShutdown
tetapi melanjutkan sisa operasinya, atauBergantung 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.
sumber