Posting Stack Overflow ini mencantumkan daftar situasi yang cukup komprehensif di mana spesifikasi bahasa C / C ++ menyatakan sebagai 'perilaku tidak terdefinisi'. Namun, saya ingin memahami mengapa bahasa modern lainnya, seperti C # atau Java, tidak memiliki konsep 'perilaku tidak terdefinisi'. Apakah ini berarti, perancang kompiler dapat mengontrol semua skenario yang mungkin (C # dan Java) atau tidak (C dan C ++)?
50
nullptr
) tidak ada seseorang peduli untuk mendefinisikan perilaku dengan menulis dan / atau mengadopsi spesifikasi yang diusulkan ". : cJawaban:
Perilaku tidak terdefinisi adalah salah satu dari hal-hal yang diakui sebagai ide yang sangat buruk hanya dalam retrospeksi.
Kompiler pertama adalah pencapaian besar dan dengan gembira menyambut peningkatan atas bahasa mesin alternatif atau pemrograman bahasa assembly. Masalah dengan yang terkenal, dan bahasa tingkat tinggi diciptakan khusus untuk memecahkan masalah yang diketahui. (Antusiasme pada saat itu begitu besar sehingga HLL kadang-kadang dipuji sebagai "akhir pemrograman" - seolah-olah mulai sekarang kita hanya perlu dengan sepele menuliskan apa yang kita inginkan dan kompiler akan melakukan semua pekerjaan nyata.)
Baru kemudian kami menyadari masalah baru yang datang dengan pendekatan yang lebih baru. Menjadi jauh dari mesin aktual yang menjalankan kode berarti ada lebih banyak kemungkinan hal diam-diam tidak melakukan apa yang kita harapkan. Misalnya, mengalokasikan variabel biasanya akan membuat nilai awal tidak terdefinisi; ini tidak dianggap sebagai masalah, karena Anda tidak akan mengalokasikan variabel jika Anda tidak ingin menyimpan nilai di dalamnya, kan? Tentunya tidak terlalu berharap bahwa programmer profesional tidak akan lupa untuk menetapkan nilai awal, bukan?
Ternyata dengan basis kode yang lebih besar dan struktur yang lebih rumit yang menjadi mungkin dengan sistem pemrograman yang lebih kuat, ya, banyak programmer memang akan melakukan pengawasan seperti itu dari waktu ke waktu, dan perilaku yang tidak terdefinisi yang dihasilkan menjadi masalah besar. Bahkan saat ini, sebagian besar kebocoran keamanan dari kecil ke mengerikan adalah hasil dari perilaku yang tidak terdefinisi dalam satu atau lain bentuk. (Alasannya adalah bahwa biasanya, perilaku tidak terdefinisi sebenarnya sangat banyak ditentukan oleh hal-hal pada tingkat yang lebih rendah berikutnya pada komputasi, dan penyerang yang memahami tingkat itu dapat menggunakan ruang gerak untuk membuat program tidak hanya hal-hal yang tidak disengaja, tetapi justru hal-hal yang tepat. mereka berniat.)
Sejak kami menyadari hal ini, ada dorongan umum untuk membuang perilaku tidak terdefinisi dari bahasa tingkat tinggi, dan Jawa sangat teliti tentang hal ini (yang relatif mudah karena dirancang untuk berjalan pada mesin virtual yang dirancang khusus pula). Bahasa lama seperti C tidak dapat dengan mudah dipasang seperti itu tanpa kehilangan kompatibilitas dengan sejumlah besar kode yang ada.
Sunting: Seperti yang ditunjukkan, efisiensi adalah alasan lain. Perilaku tidak terdefinisi berarti bahwa penulis kompiler memiliki banyak peluang untuk mengeksploitasi arsitektur target sehingga setiap implementasi lolos dengan implementasi tercepat dari setiap fitur. Ini lebih penting pada mesin-mesin yang kurang bertenaga kemarin dibandingkan dengan saat ini, ketika gaji programmer sering menjadi penghambat pengembangan perangkat lunak.
sumber
int32_t add(int32_t x, int32_t y)
) di C ++. Argumen yang biasa di sekitar yang terkait dengan efisiensi, tetapi sering diselingi dengan beberapa argumen portabilitas (seperti dalam "Tulis sekali, jalankan ... pada platform di mana Anda menulisnya ... dan tempat lain ;-)"). Oleh karena itu, satu argumen dapat berupa: Beberapa hal tidak terdefinisi karena Anda tidak tahu apakah Anda menggunakan mikrokontroler 16bit atau server 64bit (yang lemah, tetapi masih argumen)Pada dasarnya karena perancang Jawa dan bahasa serupa tidak ingin perilaku yang tidak terdefinisi dalam bahasa mereka. Ini adalah trade off - memungkinkan perilaku yang tidak terdefinisi memiliki potensi untuk meningkatkan kinerja, tetapi desainer bahasa memprioritaskan keselamatan dan kepastian yang lebih tinggi.
Misalnya, jika Anda mengalokasikan array di C, data tidak ditentukan. Di Jawa, semua byte harus diinisialisasi ke 0 (atau nilai tertentu lainnya). Ini berarti runtime harus melewati array (operasi O (n)), sementara C dapat melakukan alokasi dalam sekejap. Jadi C akan selalu lebih cepat untuk operasi seperti itu.
Jika kode yang menggunakan array akan tetap mengisi sebelum membaca, ini pada dasarnya adalah usaha yang sia-sia untuk Java. Tetapi dalam kasus di mana kode membaca terlebih dahulu, Anda mendapatkan hasil yang dapat diprediksi di Jawa tetapi hasil yang tidak dapat diprediksi dalam C.
sumber
valgrind
, yang akan menunjukkan dengan tepat di mana nilai yang tidak diinisialisasi digunakan. Anda tidak dapat menggunakanvalgrind
kode java karena runtime melakukan inisialisasi, membuatvalgrind
cek tidak berguna.Perilaku yang tidak terdefinisi memungkinkan pengoptimalan yang signifikan, dengan memberikan garis kompiler untuk melakukan sesuatu yang aneh atau tidak terduga (atau bahkan normal) pada batas tertentu atau kondisi lainnya.
Lihat http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
sumber
a + b
untuk dikompilasi denganadd b a
instruksi asli dalam setiap situasi, daripada berpotensi membutuhkan kompiler untuk mensimulasikan beberapa bentuk aritmatika integer lain yang ditandatangani.HashSet
sangat bagus.<<
mungkin merupakan kasus yang sulit.x << y
mengevaluasi beberapa nilai yang valid dari tipe tersebutint32_t
tetapi kami tidak akan mengatakan yang mana". Hal ini memungkinkan pelaksana untuk menggunakan solusi cepat, tetapi tidak bertindak sebagai prasyarat palsu yang memungkinkan optimasi gaya perjalanan-waktu karena nondeterminisme dibatasi pada output dari operasi yang satu ini - spesifikasi menjamin bahwa memori, variabel volatil, dll tidak terlihat terpengaruh oleh evaluasi ekspresi. ...Pada hari-hari awal C, ada banyak kekacauan. Kompiler yang berbeda memperlakukan bahasa secara berbeda. Ketika ada minat untuk menulis spesifikasi untuk bahasa tersebut, spesifikasi itu harus cukup kompatibel dengan C yang diandalkan oleh programmer dengan kompiler mereka. Tetapi beberapa perincian tersebut bersifat non-portabel dan tidak masuk akal secara umum, misalnya dengan asumsi endianess atau tata letak data tertentu. Oleh karena itu, standar C menyimpan banyak detail sebagai perilaku yang tidak ditentukan atau ditentukan implementasi, yang memberikan banyak fleksibilitas kepada penulis kompiler. C ++ dibangun di atas C dan juga menampilkan perilaku yang tidak terdefinisi.
Java mencoba menjadi bahasa yang jauh lebih aman dan lebih sederhana daripada C ++. Java mendefinisikan semantik bahasa dalam hal mesin virtual yang menyeluruh. Ini menyisakan sedikit ruang untuk perilaku yang tidak terdefinisi, di sisi lain itu membuat persyaratan yang bisa sulit untuk implementasi Java untuk dilakukan (misalnya bahwa tugas referensi harus atomik, atau bagaimana bilangan bulat bekerja). Di mana Java mendukung operasi yang berpotensi tidak aman, mereka biasanya diperiksa oleh mesin virtual saat runtime (misalnya, beberapa gips).
sumber
this
null?" Mengecek beberapa waktu lalu, dengan alasan bahwathis
itunullptr
adalah UB, dan dengan demikian tidak akan pernah benar-benar terjadi.)Bahasa JVM dan .NET membuatnya mudah:
Ada beberapa poin bagus untuk pilihan ini:
Ketika pintu keluar disediakan, mereka mengundang perilaku tidak terdefinisi penuh kembali. Tapi setidaknya mereka umumnya hanya digunakan dalam beberapa peregangan sangat pendek, yang dengan demikian lebih mudah untuk memverifikasi secara manual.
sumber
unsafe
kata kunci atau atribut diSystem.Runtime.InteropServices
). Dengan menyimpan hal-hal ini kepada beberapa programmer yang tahu cara men-debug hal-hal yang tidak dikelola dan sekali lagi praktis, kami menyimpan masalah. Sudah lebih dari 10 tahun sejak palu tidak aman terkait kinerja terakhir tetapi kadang-kadang Anda harus melakukannya karena secara harfiah tidak ada solusi lain.Java dan C # dicirikan oleh vendor yang dominan, setidaknya pada awal pengembangannya. (Sun dan Microsoft masing-masing). C dan C ++ berbeda; mereka sudah memiliki beberapa implementasi yang bersaing sejak awal. C terutama berlari pada platform perangkat keras yang eksotis juga. Akibatnya, ada variasi antara implementasi. Komite ISO yang menstandarkan C dan C ++ dapat menyepakati denominator bersama yang besar, tetapi pada ujung-ujungnya di mana implementasi berbeda dengan standar, ruang yang tersisa untuk implementasi.
Ini juga karena memilih satu perilaku mungkin mahal pada arsitektur perangkat keras yang bias terhadap pilihan lain - endianness adalah pilihan yang jelas.
sumber
Alasan sebenarnya datang ke perbedaan mendasar dalam niat antara C dan C ++ di satu sisi, dan Java dan C # (hanya untuk beberapa contoh) di sisi lain. Untuk alasan historis, banyak diskusi di sini berbicara tentang C daripada C ++, tetapi (karena Anda mungkin sudah tahu) C ++ adalah keturunan C yang cukup langsung, jadi apa yang dikatakan tentang C berlaku sama untuk C ++.
Meskipun mereka sebagian besar dilupakan (dan keberadaan mereka kadang-kadang bahkan ditolak), versi pertama UNIX ditulis dalam bahasa assembly. Sebagian besar (jika tidak semata-mata) tujuan asli C adalah port UNIX dari bahasa assembly ke bahasa level yang lebih tinggi. Bagian dari tujuannya adalah untuk menulis sebanyak mungkin sistem operasi dalam bahasa tingkat yang lebih tinggi - atau melihatnya dari arah lain, untuk meminimalkan jumlah yang harus ditulis dalam bahasa assembly.
Untuk mencapai itu, C perlu menyediakan tingkat akses yang hampir sama ke perangkat keras seperti bahasa assembly. PDP-11 (misalnya) memetakan register I / O ke alamat tertentu. Misalnya, Anda akan membaca satu lokasi memori untuk memeriksa apakah suatu tombol telah ditekan pada konsol sistem. Satu bit diatur di lokasi itu ketika ada data yang menunggu untuk dibaca. Anda kemudian akan membaca byte dari lokasi lain yang ditentukan untuk mengambil kode ASCII dari tombol yang telah ditekan.
Demikian juga, jika Anda ingin mencetak beberapa data, Anda akan memeriksa lokasi lain yang ditentukan, dan ketika perangkat output siap, Anda akan menulis data Anda lagi lokasi lain yang ditentukan.
Untuk mendukung driver penulisan untuk perangkat tersebut, C memungkinkan Anda untuk menentukan lokasi sewenang-wenang menggunakan beberapa jenis integer, mengubahnya menjadi sebuah pointer, dan membaca atau menulis lokasi itu dalam memori.
Tentu saja, ini memiliki masalah yang cukup serius: tidak semua mesin di bumi memiliki ingatannya yang identik dengan PDP-11 dari awal 1970-an. Jadi, ketika Anda mengambil bilangan bulat itu, mengonversinya menjadi sebuah pointer, dan kemudian membaca atau menulis melalui pointer itu, tidak ada yang bisa memberikan jaminan yang masuk akal tentang apa yang akan Anda dapatkan. Hanya untuk contoh yang jelas, membaca dan menulis dapat dipetakan ke register terpisah di perangkat keras, sehingga Anda (bertentangan dengan memori normal) jika Anda menulis sesuatu, kemudian mencoba membacanya kembali, apa yang Anda baca mungkin tidak cocok dengan apa yang Anda tulis.
Saya dapat melihat beberapa kemungkinan yang tersisa:
Dari jumlah tersebut, 1 tampaknya tidak masuk akal sehingga sulit untuk didiskusikan lebih lanjut. 2 pada dasarnya membuang niat dasar bahasa tersebut. Itu meninggalkan opsi ketiga sebagai satu-satunya yang mereka anggap masuk akal.
Poin lain yang cukup sering muncul adalah ukuran tipe integer. C mengambil "posisi" yang
int
seharusnya merupakan ukuran alami yang disarankan oleh arsitektur. Jadi, jika saya memprogram VAX 32-bit,int
mungkin seharusnya 32 bit, tetapi jika saya memprogram 36-bit Univac,int
mungkin harus 36 bit (dan seterusnya). Mungkin tidak masuk akal (dan bahkan mungkin tidak mungkin) untuk menulis sistem operasi untuk komputer 36-bit hanya menggunakan tipe yang dijamin kelipatan 8 bit. Mungkin saya hanya menjadi dangkal, tetapi bagi saya sepertinya jika saya menulis OS untuk mesin 36-bit, saya mungkin ingin menggunakan bahasa yang mendukung tipe 36-bit.Dari sudut pandang bahasa, ini mengarah pada perilaku yang lebih tidak terdefinisi. Jika saya mengambil nilai terbesar yang akan masuk ke dalam 32 bit, apa yang akan terjadi ketika saya menambahkan 1? Pada perangkat keras 32-bit yang khas, itu akan berguling (atau mungkin melemparkan semacam kesalahan perangkat keras). Di sisi lain, jika itu berjalan pada perangkat keras 36-bit, itu hanya akan ... menambahkan satu. Jika bahasa tersebut akan mendukung sistem operasi penulisan, Anda tidak dapat menjamin perilaku mana pun - Anda harus membiarkan ukuran jenis dan perilaku overflow bervariasi dari satu ke yang lain.
Java dan C # dapat mengabaikan semua itu. Mereka tidak dimaksudkan untuk mendukung sistem operasi penulisan. Dengan mereka, Anda memiliki beberapa pilihan. Salah satunya adalah membuat perangkat keras mendukung apa yang mereka inginkan - karena mereka menuntut jenis yang 8, 16, 32 dan 64 bit, buat saja perangkat keras yang mendukung ukuran tersebut. Kemungkinan lain yang jelas adalah agar bahasa hanya berjalan di atas perangkat lunak lain yang menyediakan lingkungan yang mereka inginkan, terlepas dari apa yang mungkin diinginkan perangkat keras yang mendasarinya.
Dalam kebanyakan kasus, ini sebenarnya bukan pilihan baik / atau. Sebaliknya, banyak implementasi melakukan sedikit dari keduanya. Anda biasanya menjalankan Java pada JVM yang berjalan pada sistem operasi. Lebih sering daripada tidak, OS ditulis dalam C, dan JVM dalam C ++. Jika JVM berjalan pada CPU ARM, kemungkinan cukup bagus bahwa CPU menyertakan ekstensi Jazelle ARM, untuk menyesuaikan perangkat keras lebih dekat dengan kebutuhan Java, jadi lebih sedikit yang perlu dilakukan dalam perangkat lunak, dan kode Java berjalan lebih cepat (atau kurang lambat, pokoknya).
Ringkasan
C dan C ++ memiliki perilaku yang tidak jelas, karena tidak ada yang mendefinisikan alternatif yang dapat diterima yang memungkinkan mereka untuk melakukan apa yang seharusnya mereka lakukan. C # dan Java mengambil pendekatan yang berbeda, tetapi pendekatan itu kurang cocok (jika sama sekali) dengan tujuan C dan C ++. Secara khusus, tampaknya tidak ada cara yang masuk akal untuk menulis perangkat lunak sistem (seperti sistem operasi) pada sebagian besar perangkat keras yang dipilih secara sewenang-wenang. Keduanya biasanya tergantung pada fasilitas yang disediakan oleh perangkat lunak sistem yang ada (biasanya ditulis dalam C atau C ++) untuk melakukan pekerjaan mereka.
sumber
Para penulis C Standard mengharapkan pembacanya untuk mengenali sesuatu yang mereka pikir sudah jelas, dan disinggung dalam Rationale mereka yang diterbitkan, tetapi tidak mengatakan secara langsung: Komite tidak perlu memesan penulis kompiler untuk memenuhi kebutuhan pelanggan mereka, karena pelanggan harus tahu lebih baik daripada Komite apa kebutuhan mereka. Jika jelas bahwa penyusun jenis plaform tertentu diharapkan memproses konstruk dengan cara tertentu, tidak seorang pun akan peduli apakah Standar mengatakan bahwa konstruk itu memanggil Perilaku Tidak Terdefinisi. Kegagalan Standar untuk mengamanatkan bahwa penyesuai penyesuai memproses sepotong kode dengan bermanfaat sama sekali tidak menyiratkan bahwa pemrogram harus mau membeli penyusun yang tidak.
Pendekatan desain bahasa ini bekerja sangat baik di dunia di mana penulis kompiler harus menjual barang mereka kepada pelanggan yang membayar. Ini benar-benar berantakan di dunia di mana penulis kompiler terisolasi dari efek pasar. Sangat diragukan kondisi pasar yang tepat akan pernah ada untuk mengarahkan bahasa dengan cara mereka mengarahkan bahasa yang menjadi populer pada 1990-an, dan bahkan lebih ragu bahwa perancang bahasa yang waras ingin bergantung pada kondisi pasar seperti itu.
sumber
C ++ dan c keduanya memiliki standar deskriptif (versi ISO, pokoknya).
Yang hanya ada untuk menjelaskan cara kerja bahasa, dan untuk memberikan referensi tunggal tentang apa bahasa itu. Biasanya, vendor penyusun, dan penulis perpustakaan, memimpin dan beberapa saran disertakan dalam standar ISO utama.
Java dan C # (atau Visual C #, yang saya asumsikan maksud Anda) memiliki standar preskriptif . Mereka memberi tahu Anda apa yang ada dalam bahasa tersebut sebelumnya, cara kerjanya, dan apa yang dianggap sebagai perilaku yang diizinkan.
Lebih penting dari itu, Java sebenarnya memiliki "implementasi referensi" di Open-JDK. (Saya pikir Roslyn dianggap sebagai implementasi referensi Visual C #, tetapi tidak dapat menemukan sumber untuk itu.)
Dalam kasus Java, jika ada ambiguitas dalam standar, dan Open-JDK melakukannya dengan cara tertentu. Cara Open-JDK melakukannya adalah standar.
sumber
Perilaku tidak terdefinisi memungkinkan kompiler untuk menghasilkan kode yang sangat efisien pada berbagai arsitek. Jawaban Erik menyebutkan optimasi, tetapi lebih dari itu.
Sebagai contoh, overflow yang ditandatangani adalah perilaku yang tidak terdefinisi dalam C. Dalam praktiknya kompiler diharapkan untuk menghasilkan opcode tambahan sederhana yang ditandatangani untuk dijalankan oleh CPU, dan perilaku tersebut akan menjadi apa pun yang dilakukan CPU tertentu.
Itu memungkinkan C untuk berkinerja sangat baik dan menghasilkan kode yang sangat ringkas pada sebagian besar arsitektur. Jika standar telah menetapkan bahwa bilangan bulat yang ditandatangani harus meluap dengan cara tertentu maka CPU yang berperilaku berbeda akan membutuhkan lebih banyak menghasilkan kode untuk penambahan yang ditandatangani sederhana.
Itulah alasan banyak perilaku tidak terdefinisi dalam C, dan mengapa hal-hal seperti ukuran
int
bervariasi di antara sistem.Int
tergantung arsitektur dan umumnya dipilih untuk menjadi tipe data tercepat, paling efisien yang lebih besar dari achar
.Kembali ketika C baru pertimbangan ini penting. Komputer kurang kuat, seringkali memiliki kecepatan pemrosesan dan memori yang terbatas. C digunakan di mana kinerja benar-benar penting, dan pengembang diharapkan untuk memahami bagaimana komputer bekerja dengan cukup baik untuk mengetahui apa sebenarnya perilaku tidak terdefinisi ini pada sistem mereka.
Bahasa-bahasa selanjutnya seperti Java dan C # lebih disukai menghilangkan perilaku tidak terdefinisi daripada kinerja mentah.
sumber
Dalam arti tertentu, Java juga memilikinya. Misalkan, Anda memberi pembanding yang salah ke Arrays.sort. Itu bisa melempar pengecualian mendeteksi itu. Kalau tidak, ia akan mengurutkan array dengan cara yang tidak dijamin khusus.
Demikian pula jika Anda memodifikasi variabel dari beberapa utas, hasilnya juga tidak dapat diprediksi.
C ++ hanya melangkah lebih jauh untuk membuat lebih banyak situasi yang tidak terdefinisi (atau lebih tepatnya java memutuskan untuk mendefinisikan lebih banyak operasi) dan memiliki nama untuk itu.
sumber
a
menjadi perilaku yang tidak terdefinisi jika Anda bisa mendapatkan 51 atau 73 dari itu, tetapi jika Anda hanya bisa mendapatkan 53 atau 71, itu didefinisikan dengan baik.