Mengapa C ++ memiliki 'perilaku tidak terdefinisi' (UB) dan bahasa lain seperti C # atau Java tidak?

50

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 ++)?

Sisir
sumber
3
namun postingan SO ini mengacu pada perilaku yang tidak terdefinisi bahkan di spec Java!
gbjbaanb
"Mengapa C ++ memiliki 'Perilaku Tidak Terdefinisi'" Sayangnya, ini tampaknya menjadi salah satu pertanyaan yang sulit dijawab secara obyektif, di luar pernyataan "karena, untuk alasan X, Y, dan / atau Z (semuanya mungkin nullptr) tidak ada seseorang peduli untuk mendefinisikan perilaku dengan menulis dan / atau mengadopsi spesifikasi yang diusulkan ". : c
code_dredd
Saya akan menantang premis. Setidaknya C # memiliki kode "tidak aman". Microsoft menulis "Dalam arti tertentu, menulis kode yang tidak aman seperti menulis kode C dalam program C #" dan memberikan contoh alasan mengapa seseorang ingin melakukannya: untuk mengakses perangkat keras atau OS dan untuk kecepatan. Inilah yang diciptakan oleh C (sih, mereka menulis OS dalam C!), Jadi begitulah.
Peter - Reinstate Monica

Jawaban:

72

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.

Kilian Foth
sumber
56
Saya tidak berpikir bahwa banyak orang dari komunitas C akan setuju dengan pernyataan ini. Jika Anda melakukan retrofit C dan mendefinisikan perilaku yang tidak terdefinisi (mis., Menginisialisasi default semuanya, memilih urutan evaluasi untuk parameter fungsi, dll), basis besar kode yang berperilaku baik akan terus bekerja dengan baik. Hanya kode yang tidak didefinisikan dengan baik hari ini yang akan terganggu. Di sisi lain, jika Anda meninggalkan yang tidak ditentukan seperti hari ini, kompiler akan terus bebas untuk mengeksploitasi kemajuan baru dalam arsitektur CPU dan optimasi kode.
Christophe
13
Bagian utama dari jawabannya tidak terlalu meyakinkan bagi saya. Maksud saya, pada dasarnya tidak mungkin untuk menulis fungsi yang secara aman menambahkan dua angka (seperti pada 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)
Marco13
12
@ Marco13 Setuju - dan menyingkirkan masalah "perilaku tidak terdefinisi" dengan membuat sesuatu "perilaku yang didefinisikan, tetapi tidak selalu apa yang diinginkan pengguna dan tanpa peringatan ketika itu terjadi" alih-alih "perilaku tidak terdefinisi" hanya bermain permainan kode-pengacara IMO .
alephzero
9
"Bahkan hari ini, sebagian besar kebocoran keamanan dari kecil ke mengerikan adalah hasil dari perilaku yang tidak terdefinisi dalam satu atau lain bentuk." Kutipan diperlukan. Saya pikir kebanyakan dari mereka adalah injeksi XYZ sekarang.
Yosua
34
"Perilaku tidak terdefinisi adalah salah satu dari hal-hal yang diakui sebagai ide yang sangat buruk hanya dalam retrospeksi." Itu pendapat mu. Banyak (termasuk saya) tidak membagikannya.
Lightness Races dengan Monica
103

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.

JacquesB
sumber
19
Presentasi istimewa dari dilema HLL: keamanan dan kemudahan penggunaan vs. kinerja. Tidak ada peluru perak: ada kasus penggunaan untuk setiap sisi.
Christophe
5
@ Christophe Agar adil, ada banyak pendekatan yang lebih baik untuk masalah daripada membiarkan UB benar-benar tidak terbantahkan seperti C dan C ++. Anda bisa memiliki bahasa yang aman dan terkelola, dengan jalan keluar menuju wilayah yang tidak aman, untuk Anda terapkan di tempat yang bermanfaat. TBH, itu akan sangat bagus untuk hanya dapat mengkompilasi program C / C ++ saya dengan bendera yang mengatakan "masukkan mesin runtime mahal apa pun yang Anda butuhkan, saya tidak peduli, tetapi katakan saja tentang SEMUA UB yang terjadi . "
Alexander
4
Contoh yang baik dari struktur data yang dengan sengaja membaca lokasi yang tidak diinisialisasi adalah representasi himpunan jarang Briggs dan Torczon (mis. Lihat codingplayground.blogspot.com/2009/03/… ). Inisialisasi himpunan tersebut adalah O (1) di C, tetapi O ( n) dengan inisialisasi paksa Java.
Arch D. Robison
9
Meskipun benar bahwa memaksa inisialisasi data membuat program yang rusak jauh lebih dapat diprediksi, itu tidak menjamin perilaku yang dimaksud: Jika algoritma mengharapkan untuk membaca data yang bermakna sambil secara salah membaca nol yang diinisialisasi secara tersirat, itu sama seperti bug seolah-olah memiliki baca beberapa sampah. Dengan program C / C ++ bug seperti itu akan terlihat dengan menjalankan proses di bawah valgrind, yang akan menunjukkan dengan tepat di mana nilai yang tidak diinisialisasi digunakan. Anda tidak dapat menggunakan valgrindkode java karena runtime melakukan inisialisasi, membuat valgrindcek tidak berguna.
cmaster
5
@cmaster Itulah sebabnya kompiler C # tidak memungkinkan Anda membaca dari penduduk lokal yang belum diinisialisasi. Tidak perlu pemeriksaan runtime, tidak perlu inisialisasi, hanya analisis waktu kompilasi. Namun, ini masih merupakan trade-off - ada beberapa kasus di mana Anda tidak memiliki cara yang baik untuk menangani percabangan di sekitar penduduk lokal yang berpotensi tidak ditugaskan. Dalam praktiknya, saya belum menemukan kasus di mana ini bukan desain yang buruk di tempat pertama dan lebih baik diselesaikan dengan memikirkan kembali kode untuk menghindari percabangan yang rumit (yang sulit bagi manusia untuk diurai), tetapi setidaknya mungkin.
Luaan
42

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

Penggunaan variabel tidak diinisialisasi: Ini biasanya dikenal sebagai sumber masalah dalam program C dan ada banyak alat untuk menangkap ini: dari peringatan kompiler ke penganalisa statis dan dinamis. Ini meningkatkan kinerja dengan tidak mengharuskan semua variabel nol diinisialisasi ketika mereka datang ke ruang lingkup (seperti yang dilakukan Java). Untuk sebagian besar variabel skalar, ini akan menyebabkan sedikit overhead, tetapi tumpukan array dan memori malloc'd akan menimbulkan memset penyimpanan, yang bisa sangat mahal, terutama karena penyimpanan biasanya sepenuhnya ditimpa.


Signed integer overflow: Jika aritmatika pada tipe 'int' (misalnya) meluap, hasilnya tidak ditentukan. Salah satu contoh adalah bahwa "INT_MAX +1" tidak dijamin menjadi INT_MIN. Perilaku ini memungkinkan kelas optimasi tertentu yang penting untuk beberapa kode. Misalnya, mengetahui bahwa INT_MAX + 1 tidak terdefinisi memungkinkan pengoptimalan "X + 1> X" menjadi "true". Mengetahui multiplikasi "tidak bisa" meluap (karena hal itu akan tidak terdefinisi) memungkinkan pengoptimalan "X * 2/2" menjadi "X". Meskipun ini mungkin tampak sepele, hal-hal semacam ini biasanya diekspos oleh inlining dan ekspansi makro. Pengoptimalan yang lebih penting yang memungkinkan ini adalah untuk "<=" loop seperti ini:

for (i = 0; i <= N; ++i) { ... }

Dalam loop ini, kompilator dapat mengasumsikan bahwa loop akan mengulangi tepat N + 1 kali jika "i" tidak didefinisikan pada overflow, yang memungkinkan berbagai optimasi loop untuk menendang. Di sisi lain, jika variabel didefinisikan untuk membungkus overflow, maka kompiler harus mengasumsikan bahwa loop mungkin tak terbatas (yang terjadi jika N adalah INT_MAX) - yang kemudian menonaktifkan optimisasi loop penting ini. Ini khususnya mempengaruhi platform 64-bit karena begitu banyak kode menggunakan "int" sebagai variabel induksi.

Erik Eidt
sumber
27
Tentu saja, alasan sebenarnya mengapa sign-integer overflow tidak terdefinisi adalah bahwa ketika C dikembangkan, setidaknya ada tiga representasi berbeda dari integer yang ditandatangani yang digunakan (satu-pelengkap, dua-pelengkap, dua-sign, magnitudo-sign, dan mungkin offset biner) , dan masing-masing memberikan hasil yang berbeda untuk INT_MAX +1. Membuat overflow izin yang tidak ditentukan a + buntuk dikompilasi dengan add b ainstruksi asli dalam setiap situasi, daripada berpotensi membutuhkan kompiler untuk mensimulasikan beberapa bentuk aritmatika integer lain yang ditandatangani.
Mark
2
Mengizinkan bilangan bulat bilangan bulat berperilaku dengan cara yang didefinisikan secara longgar memungkinkan optimalisasi yang signifikan dalam kasus di mana semua perilaku yang mungkin akan memenuhi persyaratan aplikasi . Namun, sebagian besar optimasi itu akan hangus, jika programmer diminta untuk menghindari integer overflow di semua biaya.
supercat
5
@supercat Yang merupakan alasan lain mengapa menghindari perilaku tidak terdefinisi lebih umum dalam bahasa yang lebih baru - waktu programmer dihargai jauh lebih banyak daripada waktu CPU. Jenis optimisasi C yang diperbolehkan untuk dilakukan berkat UB pada dasarnya tidak ada gunanya pada komputer desktop modern, dan membuat alasan tentang kode jauh lebih sulit (belum lagi implikasi keamanan). Bahkan dalam kode kritis kinerja, Anda dapat mengambil manfaat dari optimasi tingkat tinggi yang akan sedikit lebih sulit (atau bahkan lebih sulit) untuk dilakukan dalam C. Saya memiliki perender 3D perangkat lunak saya sendiri di C #, dan dapat menggunakan misalnya a HashSetsangat bagus.
Luaan
2
@supercat: Wrt_loosely defined_, pilihan logis untuk integer overflow adalah membutuhkan Implementasi Perilaku yang Ditentukan . Itu adalah konsep yang sudah ada, dan itu bukan beban yang tidak semestinya pada implementasi. Sebagian besar akan lolos dengan "itu 2's melengkapi dengan membungkus", saya kira. <<mungkin merupakan kasus yang sulit.
MSalters
@ MSalters Ada solusi sederhana dan dipelajari dengan baik yang bukan perilaku tidak terdefinisi atau implementasi perilaku didefinisikan: perilaku nondeterministic. Artinya, Anda dapat mengatakan " x << ymengevaluasi beberapa nilai yang valid dari tipe tersebut int32_ttetapi 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. ...
Mario Carneiro
20

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).

amon
sumber
Jadi, apakah Anda mengatakan, kompatibilitas ke belakang adalah satu-satunya alasan mengapa C dan C ++ tidak keluar dari perilaku yang tidak terdefinisi?
Sisir
3
Pasti salah satu yang lebih besar, @Sisir. Bahkan di antara programer berpengalaman, Anda akan terkejut berapa banyak barang yang seharusnya tidak pecah tidak istirahat ketika kompilator mengubah bagaimana menangani perilaku undefined. (Contohnya, ada sedikit kekacauan ketika GCC mulai mengoptimalkan "is thisnull?" Mengecek beberapa waktu lalu, dengan alasan bahwa thisitu nullptradalah UB, dan dengan demikian tidak akan pernah benar-benar terjadi.)
Justin Time 2 Reinstate Monica
9
@Sisir, yang besar lainnya adalah kecepatan. Pada masa-masa awal C, perangkat keras jauh lebih heterogen daripada sekarang. Dengan tidak menentukan apa yang terjadi ketika Anda menambahkan 1 ke INT_MAX, Anda dapat membiarkan kompiler melakukan apa pun yang tercepat untuk arsitektur (mis. Sistem komplemen seseorang akan menghasilkan -INT_MAX, sementara sistem dua komplemen akan menghasilkan INT_MIN). Demikian pula, dengan tidak menentukan apa yang terjadi ketika Anda membaca melewati akhir array, Anda dapat memiliki sistem dengan perlindungan memori mengakhiri program, sementara yang tanpa tidak perlu menerapkan pengecekan batas runtime yang mahal.
Tandai
14

Bahasa JVM dan .NET membuatnya mudah:

  1. Mereka tidak harus dapat bekerja secara langsung dengan perangkat keras.
  2. Mereka hanya harus bekerja dengan sistem desktop dan server modern atau perangkat yang cukup mirip, atau setidaknya perangkat yang dirancang untuk mereka.
  3. Mereka dapat memaksakan pengumpulan sampah untuk semua memori, dan memaksa inisialisasi, sehingga mendapatkan keamanan pointer.
  4. Mereka ditentukan oleh aktor tunggal yang juga menyediakan implementasi definitif tunggal.
  5. Mereka bisa memilih keamanan daripada kinerja.

Ada beberapa poin bagus untuk pilihan ini:

  1. Pemrograman sistem adalah ballgame yang sama sekali berbeda, dan mengoptimalkan tanpa kompromi untuk pemrograman aplikasi adalah wajar.
  2. Memang, ada perangkat keras yang kurang eksotis sepanjang waktu, tetapi sistem tertanam kecil di sini untuk tinggal.
  3. GC tidak cocok untuk sumber daya yang tidak dapat dipertukarkan, dan memperdagangkan lebih banyak ruang untuk kinerja yang baik. Dan sebagian besar (tetapi tidak hampir semua) inisialisasi paksa dapat dioptimalkan.
  4. Ada keuntungan untuk lebih banyak kompetisi, tetapi komite berarti kompromi.
  5. Semua pemeriksaan batas itu bertambah, meskipun sebagian besar dapat dioptimalkan. Pemeriksaan null pointer sebagian besar dapat dilakukan dengan menjebak akses untuk overhead nol berkat ruang alamat virtual, meskipun optimasi masih terhambat.

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.

Deduplicator
sumber
3
Memang. Saya memprogram dalam C # untuk pekerjaan saya. Sesekali saya meraih salah satu palu yang tidak aman ( unsafekata kunci atau atribut di System.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.
Joshua
19
Saya sering bekerja pada platform dari perangkat analog di mana sizeof (char) == sizeof (pendek) == sizeof (int) == sizeof (float) == 1. Ini juga tidak menambah jenuh (jadi INT_MAX + 1 == INT_MAX) , dan hal yang menyenangkan tentang C adalah bahwa saya dapat memiliki kompiler yang sesuai yang menghasilkan kode yang masuk akal. Jika bahasa yang diamanatkan mengatakan dua komplemen dengan membungkus maka setiap penambahan akan berakhir dengan tes dan cabang, sesuatu yang bukan starter di bagian fokus DSP. Ini adalah bagian produksi saat ini.
Dan Mills
5
@BenVoigt Sebagian dari kita hidup di dunia di mana komputer kecil mungkin 4k ruang kode, tumpukan panggilan / pengembalian 8 level tetap, 64 byte RAM, jam 1MHz, dan biaya <$ 0,20 dalam jumlah 1.000. Ponsel modern adalah PC kecil dengan penyimpanan tidak terbatas yang cukup banyak untuk semua maksud dan tujuan, dan dapat diperlakukan sebagai PC. Tidak semua dunia multicore dan tidak memiliki kendala waktu nyata yang sulit.
Dan Mills
2
@DanMills: Tidak berbicara tentang ponsel modern di sini dengan prosesor Arm Cortex A, berbicara tentang "telepon fitur" sekitar tahun 2002. Ya 192kB dari SRAM jauh lebih dari 64 byte (yang bukan "kecil" tetapi "kecil"), tetapi 192kB juga belum secara akurat disebut "modern" desktop atau server selama 30 tahun. Juga hari ini 20 sen akan memberi Anda MSP430 dengan lebih dari 64 byte SRAM.
Ben Voigt
2
@BenVoigt 192kB mungkin bukan desktop dalam 30 tahun terakhir, tetapi saya dapat meyakinkan Anda bahwa itu sepenuhnya cukup untuk melayani halaman web, yang menurut saya membuat server semacam itu dengan definisi kata yang tepat. Faktanya adalah bahwa itu adalah jumlah ram yang sepenuhnya masuk akal (murah hati, bahkan) untuk BANYAK aplikasi tertanam yang sering menyertakan konfigurasi server web. Tentu, saya mungkin tidak menjalankan amazon di atasnya, tapi saya mungkin hanya menjalankan kulkas lengkap dengan IOT crapware pada inti seperti itu (Dengan waktu dan ruang luang). Jangan sampai ada yang membutuhkan bahasa JIT untuk menerjemahkan!
Dan Mills
8

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.

MSalters
sumber
Apa yang dimaksud dengan “common denominator besar” secara harfiah ? Apakah Anda berbicara tentang himpunan bagian atau superset? Apakah Anda benar-benar berarti cukup banyak faktor yang sama? Apakah ini seperti kelipatan paling tidak umum atau faktor umum terbesar? Ini sangat membingungkan bagi kami robot yang tidak berbicara istilah jalanan, hanya matematika. :)
tchrist
@tchrist: Perilaku umum adalah himpunan bagian, tetapi himpunan ini cukup abstrak. Di banyak daerah yang tidak ditentukan oleh standar umum, implementasi nyata harus membuat pilihan. Sekarang beberapa dari pilihan itu cukup jelas dan oleh karena itu implementasi-didefinisikan, tetapi yang lain lebih kabur. Tata letak memori saat runtime adalah contoh: harus ada pilihan, tetapi tidak jelas bagaimana Anda akan mendokumentasikannya.
MSalters
2
C asli dibuat oleh satu orang. Itu sudah punya banyak UB, dengan desain. Segalanya menjadi lebih buruk karena C menjadi populer, tetapi UB ada sejak awal. Pascal dan Smalltalk memiliki UB jauh lebih sedikit dan dikembangkan pada waktu yang hampir bersamaan. Keuntungan utama C adalah sangat mudah untuk port - semua masalah portabilitas didelegasikan ke pemrogram aplikasi: P Saya bahkan telah mem-porting kompiler C sederhana ke CPU (virtual) saya; melakukan sesuatu seperti LISP atau Smalltalk akan menjadi upaya yang jauh lebih besar (meskipun saya memang memiliki prototipe terbatas untuk .NET runtime :).
Luaan
@Luaan: Apakah itu Kernighan atau Ritchie? Dan tidak, itu tidak memiliki Perilaku Tidak Terdefinisi. Saya tahu, saya memiliki dokumentasi kompiler stensil AT&T asli di meja saya. Implementasinya melakukan apa yang dilakukannya. Tidak ada perbedaan antara perilaku yang tidak ditentukan dan tidak ditentukan.
MSalters
4
@MSalters Ritchie adalah orang pertama. Kernighan hanya bergabung (tidak banyak) kemudian. Yah, itu tidak memiliki "Perilaku Tidak Terdefinisi", karena istilah itu belum ada. Tetapi memang memiliki perilaku yang sama yang hari ini akan disebut tidak terdefinisi. Karena C tidak memiliki spesifikasi, bahkan "tidak ditentukan" adalah peregangan :) Itu hanya sesuatu yang tidak diperhatikan oleh kompiler, dan detailnya tergantung pada pemrogram aplikasi. Itu tidak dirancang untuk menghasilkan aplikasi portabel , hanya kompiler yang dimaksudkan agar mudah port.
Luaan
6

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:

  1. Tentukan antarmuka untuk semua perangkat keras yang mungkin - tentukan alamat absolut dari semua lokasi yang Anda ingin baca atau tulis untuk berinteraksi dengan perangkat keras dengan cara apa pun.
  2. Larang tingkat akses itu, dan putuskan bahwa siapa pun yang ingin melakukan hal-hal seperti itu perlu menggunakan bahasa majelis.
  3. Izinkan orang melakukan itu, tetapi serahkan pada mereka untuk membaca (misalnya) manual untuk perangkat keras yang mereka targetkan, dan menulis kode agar sesuai dengan perangkat keras yang mereka gunakan.

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 intseharusnya merupakan ukuran alami yang disarankan oleh arsitektur. Jadi, jika saya memprogram VAX 32-bit, intmungkin seharusnya 32 bit, tetapi jika saya memprogram 36-bit Univac, intmungkin 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.

Jerry Coffin
sumber
4

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.

supercat
sumber
Saya merasa bahwa Anda telah menggambarkan sesuatu yang penting di sini, tetapi itu lolos dari saya. Bisakah Anda mengklarifikasi jawaban Anda? Terutama paragraf kedua: dikatakan kondisinya sekarang dan kondisinya sebelumnya berbeda, tetapi saya tidak mengerti; apa yang sebenarnya berubah? Juga, "jalan" sekarang berbeda dari sebelumnya; mungkin jelaskan ini juga?
anatolyg
4
Tampaknya kampanye Anda untuk mengganti semua perilaku tidak terdefinisi dengan perilaku yang tidak ditentukan atau sesuatu yang lebih dibatasi masih berjalan kuat.
Deduplicator
1
@anatolyg: Jika Anda belum melakukannya, baca dokumen C Rationale yang diterbitkan (ketik C99 Rationale di Google). Halaman 11 baris 23-29 berbicara tentang "pasar", dan halaman 13 baris 5-8 berbicara tentang apa yang dimaksudkan sehubungan dengan portabilitas. Menurut Anda bagaimana atasan di perusahaan pembuat kompiler komersial akan bereaksi jika penulis kompiler memberi tahu programmer yang mengeluh bahwa optimizer memecahkan kode yang ditangani oleh setiap kompiler lain dengan bermanfaat bahwa kode mereka "rusak" karena melakukan tindakan yang tidak ditentukan oleh Standar, dan menolak untuk mendukungnya karena itu akan mempromosikan lanjutan ...
supercat
1
... penggunaan konstruksi seperti itu? Sudut pandang seperti itu mudah terlihat pada papan dukungan dentang dan gcc, dan telah berfungsi untuk menghambat pengembangan intrinsik yang dapat memfasilitasi pengoptimalan jauh lebih mudah dan aman daripada bahasa yang rusak yang didukung gcc dan dentang yang ingin didukung.
supercat
1
@supercat: Anda membuang-buang nafas mengeluh kepada vendor compiler. Mengapa tidak mengarahkan kekhawatiran Anda ke komite bahasa? Jika mereka setuju dengan Anda, errata akan dikeluarkan yang dapat Anda gunakan untuk mengalahkan tim penyusun di atas kepala. Dan proses itu jauh lebih cepat daripada pengembangan versi bahasa yang baru. Tetapi jika mereka tidak setuju, Anda setidaknya akan mendapatkan alasan yang sebenarnya, sedangkan penulis kompiler hanya akan mengulangi (berulang-ulang) "Kami tidak menetapkan kode yang rusak, keputusan itu dibuat oleh komite bahasa dan kami ikuti keputusan mereka. "
Ben Voigt
3

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.

bobsburner
sumber
Situasinya lebih buruk dari itu: Saya rasa Komite tidak pernah mencapai konsensus tentang apakah itu seharusnya deskriptif atau preskriptif.
supercat
1

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 intbervariasi di antara sistem. Inttergantung arsitektur dan umumnya dipilih untuk menjadi tipe data tercepat, paling efisien yang lebih besar dari a char.

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.

pengguna
sumber
-5

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.

RiaD
sumber
4
Itu bukan perilaku yang tidak jelas yang kita bicarakan di sini. "Komparator salah" datang dalam dua jenis: yang mendefinisikan pemesanan total, dan yang tidak. Jika Anda memberikan komparator yang secara konsisten menentukan pemesanan relatif item, perilaku didefinisikan dengan baik, itu hanya bukan perilaku yang diinginkan oleh programmer. Jika Anda memberikan komparator yang tidak konsisten tentang urutan relatif, perilaku tersebut masih terdefinisi dengan baik: fungsi sortir akan memberikan pengecualian (yang juga mungkin bukan perilaku yang diinginkan oleh programmer).
Mark
2
Sedangkan untuk memodifikasi variabel, kondisi balapan umumnya tidak dianggap sebagai perilaku yang tidak terdefinisi. Saya tidak tahu detail bagaimana Java menangani tugas untuk data bersama, tetapi mengetahui filosofi umum bahasa, saya cukup yakin itu diperlukan untuk menjadi atom. Secara bersamaan menugaskan 53 dan 71 untuk amenjadi 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.
Tandai
@ Mark Dengan potongan data yang lebih besar dari ukuran kata asli sistem (misalnya, variabel 32 bit pada sistem ukuran kata 16-bit), dimungkinkan untuk memiliki arsitektur yang mengharuskan menyimpan setiap bagian 16-bit secara terpisah. (SIMD adalah situasi lain yang berpotensi seperti itu.) Dalam kasus itu, bahkan penugasan tingkat kode sumber sederhana tidak harus berupa atom kecuali jika perhatian khusus diambil oleh kompilator untuk memastikan bahwa itu dijalankan secara atom.
CVn