GCC 6 memiliki fitur pengoptimal baru : Diasumsikan this
tidak selalu nol dan dioptimalkan berdasarkan itu.
Propagasi rentang nilai sekarang mengasumsikan bahwa pointer fungsi anggota C ++ ini bukan nol. Ini menghilangkan pemeriksaan null pointer yang umum tetapi juga merusak beberapa basis kode yang tidak sesuai (seperti Qt-5, Chromium, KDevelop) . Sebagai work sementara sekitar -fno-delete-null-pointer-cek dapat digunakan. Kode yang salah dapat diidentifikasi dengan menggunakan -fsanitize = tidak terdefinisi.
Dokumen perubahan jelas menyebut ini berbahaya karena merusak sejumlah besar kode yang sering digunakan.
Mengapa asumsi baru ini mematahkan kode praktis C ++? Apakah ada pola tertentu di mana programmer yang ceroboh atau tidak informasi mengandalkan perilaku khusus yang tidak terdefinisi ini? Saya tidak dapat membayangkan siapa pun menulis if (this == NULL)
karena itu sangat tidak wajar.
sumber
this
dilewatkan sebagai parameter implisit, sehingga mereka kemudian mulai menggunakannya seolah-olah itu adalah parameter eksplisit. Ini bukan. Ketika Anda melakukan dereferensi null ini, Anda memanggil UB sama seperti jika Anda meringkas pointer nol lainnya. Itu saja yang ada di sana. Jika Anda ingin meneruskan nullptrs, gunakan parameter eksplisit, DUH . Itu tidak akan lebih lambat, tidak akan ada clunkier, dan kode yang memiliki API seperti itu jauh di dalam internal, jadi memiliki ruang lingkup yang sangat terbatas. Akhir cerita saya kira.Jawaban:
Saya kira pertanyaan yang perlu dijawab mengapa orang yang bermaksud baik akan menulis cek itu.
Kasus yang paling umum adalah jika Anda memiliki kelas yang merupakan bagian dari panggilan rekursif yang terjadi secara alami.
Jika Anda memiliki:
di C, Anda dapat menulis:
Di C ++, senang membuat ini sebagai fungsi anggota:
Pada hari-hari awal C ++ (sebelum standarisasi), ditekankan bahwa fungsi anggota adalah gula sintaksis untuk fungsi yang
this
parameternya tersirat. Kode ditulis dalam C ++, dikonversi ke C yang setara dan dikompilasi. Bahkan ada contoh eksplisit yang membandingkanthis
null dengan bermakna dan kompilator Cfront asli mengambil keuntungan dari ini juga. Jadi datang dari latar belakang C, pilihan yang jelas untuk cek adalah:Catatan: Bjarne Stroustrup bahkan menyebutkan bahwa aturan untuk
this
telah berubah selama bertahun-tahun di siniDan ini bekerja pada banyak kompiler selama bertahun-tahun. Ketika standardisasi terjadi, ini berubah. Dan baru-baru ini, kompiler mulai mengambil keuntungan dari memanggil fungsi anggota di mana
this
keberadaannullptr
adalah perilaku yang tidak terdefinisi, yang berarti bahwa kondisi ini selalufalse
, dan kompiler bebas untuk menghilangkannya.Itu berarti bahwa untuk melakukan traversal dari pohon ini, Anda perlu:
Lakukan semua cek sebelum menelepon
traverse_in_order
Ini berarti juga memeriksa di SETIAP situs panggilan jika Anda dapat memiliki root nol.
Jangan gunakan fungsi anggota
Ini berarti Anda sedang menulis kode gaya C lama (mungkin sebagai metode statis), dan memanggilnya dengan objek secara eksplisit sebagai parameter. misalnya. Anda kembali menulis
Node::traverse_in_order(node);
daripadanode->traverse_in_order();
di situs panggilan.Saya percaya cara termudah / paling rapi untuk memperbaiki contoh khusus ini dengan cara yang memenuhi standar adalah benar-benar menggunakan sentinel node daripada a
nullptr
.Tidak satu pun dari dua opsi pertama yang tampak menarik, dan sementara kode bisa lolos, mereka menulis kode yang buruk dengan
this == nullptr
alih - alih menggunakan perbaikan yang tepat.Saya menduga itulah bagaimana beberapa basis kode ini berkembang untuk
this == nullptr
memeriksa di dalamnya.sumber
1 == 0
perilaku yang tidak terdefinisi? Sederhana sajafalse
.this == nullptr
idiom adalah perilaku tidak terdefinisi karena Anda telah memanggil fungsi anggota pada objek nullptr sebelum itu, yang tidak terdefinisi. Dan kompiler bebas untuk menghilangkan cekthis
menjadi nullable. Saya kira mungkin ini adalah manfaat dari belajar C ++ di zaman di mana SO ada untuk mengakali bahaya UB di otak saya dan menghalangi saya untuk melakukan peretasan aneh seperti ini.Itu melakukannya karena kode "praktis" rusak dan melibatkan perilaku yang tidak terdefinisi untuk memulai. Tidak ada alasan untuk menggunakan null
this
, selain sebagai optimasi mikro, biasanya yang sangat prematur.Ini adalah praktik yang berbahaya, karena penyesuaian pointer karena hierarki kelas traversal dapat mengubah nol
this
menjadi bukan-nol. Jadi, paling tidak, kelas yang metodenya seharusnya bekerja dengan nolthis
haruslah kelas akhir tanpa kelas dasar: ia tidak bisa berasal dari apa pun, dan tidak bisa diturunkan dari mana. Kami dengan cepat beranjak dari tempat yang praktis ke tempat yang jelek .Secara praktis, kode tidak harus jelek:
Jika pohon itu kosong, alias nol
Node* root
, Anda tidak seharusnya memanggil metode non-statis apa pun. Titik. Tidak apa-apa memiliki kode pohon mirip C yang mengambil pointer contoh dengan parameter eksplisit.Argumen di sini tampaknya bermuara pada entah bagaimana perlu menulis metode non-statis pada objek yang dapat dipanggil dari null instance pointer. Tidak perlu seperti itu. Cara C-dengan-objek menulis kode seperti itu masih jauh lebih baik di dunia C ++, karena setidaknya bisa diketik dengan aman. Pada dasarnya, nol
this
adalah optimasi mikro, dengan bidang penggunaan yang sempit, yang tidak mengizinkan IMHO. Tidak ada API publik yang bergantung pada nolthis
.sumber
this
cek dipetik oleh berbagai analisa kode statis, sehingga tidak seperti jika ada yang memiliki secara manual memburu mereka semua. Tambalan mungkin beberapa ratus baris perubahan sepele.this
dereference adalah crash instan. Masalah-masalah ini akan ditemukan dengan sangat cepat bahkan jika tidak ada yang peduli untuk menjalankan analisa statis pada kode. C / C ++ mengikuti mantra "bayar hanya untuk fitur yang Anda gunakan". Jika Anda ingin cek, Anda harus eksplisit tentang mereka dan itu berarti tidak melakukannyathis
, ketika sudah terlambat, karena pengumpul menganggapthis
tidak nol. Kalau tidak, ia harus memeriksathis
, dan untuk 99,9999% kode di luar sana, cek semacam itu adalah buang-buang waktu.Dokumen itu tidak menyebutnya berbahaya. Juga tidak mengklaim bahwa ia merusak sejumlah kode yang mengejutkan . Ini hanya menunjukkan beberapa basis kode populer yang diklaim diketahui bergantung pada perilaku tidak terdefinisi ini dan akan rusak karena perubahan kecuali jika opsi penyelesaian digunakan.
Jika kode c ++ praktis bergantung pada perilaku yang tidak terdefinisi, maka perubahan pada perilaku yang tidak terdefinisi dapat merusaknya. Inilah sebabnya mengapa UB harus dihindari, bahkan ketika sebuah program bergantung padanya tampaknya berfungsi sebagaimana dimaksud.
Saya tidak tahu apakah itu tersebar luas anti- pola, tetapi programmer yang tidak informasi mungkin berpikir bahwa mereka dapat memperbaiki program mereka dari crash dengan melakukan:
Ketika bug yang sebenarnya adalah dereferencing pointer nol di tempat lain.
Saya yakin bahwa jika programmer tidak cukup informasi, mereka akan dapat datang dengan pola (anti) yang lebih maju yang mengandalkan UB ini.
Saya bisa.
sumber
if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere();
Seperti log yang mudah dibaca yang bagus dari urutan peristiwa yang tidak bisa dengan mudah diceritakan oleh debugger. Bersenang-senang men-debug ini sekarang tanpa menghabiskan berjam-jam menempatkan cek di mana-mana ketika ada nol acak tiba-tiba dalam kumpulan data besar, dalam kode yang belum Anda tulis ... Dan aturan UB tentang ini dibuat kemudian, setelah C ++ dibuat. Dulu valid.-fsanitize=null
.-fsanitize=null
mencatat kesalahan pada kartu SD / MMC pada Pin # 5,6,10,11 menggunakan SPI? Itu bukan solusi universal. Beberapa berpendapat bahwa itu bertentangan dengan prinsip-prinsip berorientasi objek untuk mengakses objek nol namun beberapa bahasa OOP memiliki objek nol yang dapat dioperasikan sehingga itu bukan aturan universal OOP. 1/2Beberapa kode "praktis" (lucu untuk mengeja "kereta") yang rusak tampak seperti ini:
dan lupa untuk memperhitungkan fakta bahwa
p->bar()
kadang - kadang mengembalikan pointer nol, yang berarti bahwa dereferencing untuk memanggilbaz()
tidak ditentukan.Tidak semua kode yang rusak berisi eksplisit
if (this == nullptr)
atauif (!p) return;
cek. Beberapa kasus hanyalah fungsi yang tidak mengakses variabel anggota apa pun, dan tampaknya berfungsi dengan baik. Sebagai contoh:Dalam kode ini ketika Anda menelepon
func<DummyImpl*>(DummyImpl*)
dengan pointer nol ada dereferensi "konseptual" pointer untuk memanggilp->DummyImpl::valid()
, tetapi pada kenyataannya bahwa fungsi anggota hanya kembalifalse
tanpa mengakses*this
. Itureturn false
bisa diuraikan dan dalam prakteknya pointer tidak perlu diakses sama sekali. Jadi dengan beberapa kompiler tampaknya berfungsi OK: tidak ada segfault untuk dereferencing nol,p->valid()
adalah salah, sehingga kode panggilando_something_else(p)
, yang memeriksa pointer nol, dan tidak melakukan apa-apa. Tidak ada kerusakan atau perilaku tak terduga yang diamati.Dengan GCC 6 Anda masih menerima panggilan
p->valid()
, tetapi kompiler sekarang menyimpulkan dari ekspresi yangp
harus non-null (jika tidakp->valid()
akan menjadi perilaku yang tidak terdefinisi) dan membuat catatan informasi tersebut. Informasi yang disimpulkan digunakan oleh pengoptimal sehingga jika panggilan untukdo_something_else(p)
digarisbawahi,if (p)
cek sekarang dianggap berlebihan, karena kompiler ingat bahwa itu bukan nol, dan dengan demikian menggariskan kode untuk:Ini sekarang benar-benar melakukan dereferensi pointer nol, dan kode yang sebelumnya berfungsi tampaknya berhenti bekerja.
Dalam contoh ini bug ada
func
, yang seharusnya memeriksa null terlebih dahulu (atau penelepon seharusnya tidak memanggilnya dengan null):Poin penting untuk diingat adalah bahwa kebanyakan optimisasi seperti ini bukan kasus dari kompiler yang mengatakan "ah, programmer menguji pointer ini terhadap null, saya akan menghapusnya hanya untuk menjengkelkan". Apa yang terjadi adalah berbagai optimisasi run-of-the-mill seperti inlining dan propagasi rentang nilai digabungkan untuk membuat cek tersebut menjadi berlebihan, karena mereka datang setelah pemeriksaan sebelumnya, atau dereferensi. Jika kompiler tahu bahwa pointer adalah bukan nol di titik A dalam suatu fungsi, dan pointer tidak berubah sebelum titik B berikutnya di fungsi yang sama, maka ia tahu itu juga bukan nol di B. Ketika inlining terjadi poin A dan B mungkin sebenarnya potongan-potongan kode yang awalnya dalam fungsi yang terpisah, tetapi sekarang digabungkan menjadi satu bagian dari kode, dan kompiler dapat menerapkan pengetahuannya bahwa pointer adalah non-null di lebih banyak tempat.
sumber
this
?this
dengan nol] " ?this
, maka gunakan saja-fsanitize=undefined
Standar C ++ rusak dalam cara-cara penting. Sayangnya, alih-alih melindungi pengguna dari masalah ini, pengembang GCC telah memilih untuk menggunakan perilaku tidak terdefinisi sebagai alasan untuk menerapkan optimisasi marjinal, bahkan ketika telah jelas dijelaskan kepada mereka betapa berbahayanya itu.
Di sini orang yang jauh lebih pintar daripada yang saya jelaskan dengan sangat terperinci. (Dia berbicara tentang C tetapi situasinya sama di sana).
Mengapa itu berbahaya?
Cukup mengkompilasi ulang kode yang sebelumnya berfungsi, kode aman dengan versi yang lebih baru dari kompiler dapat memperkenalkan kerentanan keamanan . Sementara perilaku baru dapat dinonaktifkan dengan flag, makefile yang ada tidak memiliki set flag itu, jelas. Dan karena tidak ada peringatan yang dihasilkan, tidak jelas bagi pengembang bahwa perilaku yang sebelumnya masuk akal telah berubah.
Dalam contoh ini, pengembang telah menyertakan pemeriksaan untuk integer overflow, menggunakan
assert
, yang akan menghentikan program jika disediakan panjang yang tidak valid. Tim GCC menghapus cek dengan dasar bahwa integer overflow tidak terdefinisi, oleh karena itu cek dapat dihapus. Ini menghasilkan contoh nyata nyata basis kode ini yang dibuat kembali menjadi rentan setelah masalah diperbaiki.Baca semuanya. Cukup membuat Anda menangis.
OK, tapi bagaimana dengan yang ini?
Jauh ketika, ada idiom yang cukup umum yang berbunyi seperti ini:
Jadi idiomanya adalah: Jika
pObj
bukan nol, Anda menggunakan pegangan yang dikandungnya, jika tidak, Anda menggunakan pegangan bawaan. Ini diringkas dalamGetHandle
fungsi.Kuncinya adalah bahwa memanggil fungsi non-virtual sebenarnya tidak menggunakan
this
pointer, jadi tidak ada pelanggaran akses.Saya masih belum mengerti
Ada banyak kode yang ditulis seperti itu. Jika seseorang hanya mengkompilasi ulang, tanpa mengubah saluran, setiap panggilan
DoThing(NULL)
adalah bug yang menabrak - jika Anda beruntung.Jika Anda tidak beruntung, panggilan ke bug yang menabrak menjadi kerentanan eksekusi jarak jauh.
Ini dapat terjadi bahkan secara otomatis. Anda punya sistem build otomatis, kan? Meng-upgrade ke kompiler terbaru tidak berbahaya, bukan? Tapi sekarang tidak - tidak jika kompiler Anda adalah GCC.
Baiklah, beri tahu mereka!
Mereka sudah diberitahu. Mereka melakukan ini dengan pengetahuan penuh tentang konsekuensinya.
tapi kenapa?
Siapa yang bisa bilang? Mungkin:
Atau mungkin sesuatu yang lain. Siapa yang bisa bilang?
sumber